/* ================ SCREENS ================ */

/* ── URL Param Helpers ────────────────────────────────────────
   Sync state ↔ URL search params (สำหรับแชร์ link)
──────────────────────────────────────────────────────────── */
const Urlp = {
  get: (k) => new URLSearchParams(location.search).get(k),
  // patch: { key: value | null }
  patch: (updates) => {
    const params = new URLSearchParams(location.search);
    Object.entries(updates).forEach(([k, v]) => {
      if (v === null || v === undefined || v === "") params.delete(k);
      else params.set(k, String(v));
    });
    const s = params.toString();
    history.replaceState({}, "", location.pathname + (s ? "?" + s : "") + location.hash);
  },
  // Sets ↔ "id1,id2,id3"
  setFromParam: (k) => new Set((Urlp.get(k) || "").split(",").filter(Boolean)),
  setToParam:   (set) => set.size ? [...set].join(",") : null,
  // sort {key,dir} ↔ "key:dir"
  sortFromParam: (k) => {
    const s = Urlp.get(k);
    if (!s) return { key: null, dir: "desc" };
    const [key, dir] = s.split(":");
    return { key: key || null, dir: dir === "asc" ? "asc" : "desc" };
  },
  sortToParam: (so) => so?.key ? `${so.key}:${so.dir || "desc"}` : null,
};

/* ── MultiSelect Dropdown ─────────────────────────────────────
   ใช้กรอง parent (Campaign / Ad Set) แบบ multi-select
   options: [{ id, name }]
   value:   Array<id>
   onChange: (newIds: Array<id>) => void
──────────────────────────────────────────────────────────── */
const MultiSelectDropdown = ({ label, options, value, onChange, placeholder = "ทั้งหมด", icon = null }) => {
  const [open, setOpen] = React.useState(false);
  const [search, setSearch] = React.useState("");
  const ref = React.useRef(null);
  const selectedSet = React.useMemo(() => new Set(value || []), [value]);
  const selectedCount = selectedSet.size;

  // close on outside click
  React.useEffect(() => {
    if (!open) return;
    const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", handler);
    return () => document.removeEventListener("mousedown", handler);
  }, [open]);

  const filtered = React.useMemo(() => {
    const q = search.toLowerCase().trim();
    return (options || []).filter(o => !q || o.name.toLowerCase().includes(q));
  }, [options, search]);

  const toggle = (id) => {
    const next = new Set(selectedSet);
    next.has(id) ? next.delete(id) : next.add(id);
    onChange([...next]);
  };
  const clearAll  = () => onChange([]);
  const selectAll = () => onChange(filtered.map(o => o.id));

  // Trigger label
  const triggerLabel = selectedCount === 0
    ? placeholder
    : selectedCount === 1
      ? (options.find(o => o.id === [...selectedSet][0])?.name?.slice(0, 22) || "1 รายการ")
      : `${selectedCount} ${label}`;

  return (
    <div ref={ref} style={{ position: "relative", display: "inline-block" }}>
      <button onClick={() => setOpen(v => !v)}
        style={{
          display: "flex", alignItems: "center", gap: 6,
          padding: "5px 10px",
          border: `1px solid ${selectedCount > 0 ? "var(--accent)" : "var(--border)"}`,
          borderRadius: 7,
          background: selectedCount > 0 ? "var(--accent-bg, #eef2ff)" : "var(--bg-elevated)",
          color: selectedCount > 0 ? "var(--accent)" : "var(--ink)",
          cursor: "pointer", fontSize: 12, fontWeight: 500, whiteSpace: "nowrap",
        }}>
        {icon}
        <span style={{ fontSize: 10, opacity: 0.7 }}>{label}:</span>
        <span>{triggerLabel}</span>
        {selectedCount > 1 && (
          <span style={{ background: "var(--accent)", color: "#fff", borderRadius: 10, padding: "0 6px", fontSize: 10, fontWeight: 600 }}>
            {selectedCount}
          </span>
        )}
        <svg width="9" height="9" viewBox="0 0 24 24" fill="currentColor" style={{ transform: open ? "rotate(180deg)" : "", transition: "transform 0.15s" }}><path d="M7 10L12 15L17 10Z"/></svg>
      </button>

      {open && (
        <div style={{
          position: "absolute", top: "100%", left: 0, marginTop: 4,
          minWidth: 280, maxWidth: 380,
          background: "var(--bg)", border: "1px solid var(--border)",
          borderRadius: 8, boxShadow: "0 6px 20px rgba(0,0,0,.12)",
          zIndex: 100, overflow: "hidden",
        }}>
          {/* Search */}
          <div style={{ padding: 8, borderBottom: "1px solid var(--border)" }}>
            <input autoFocus value={search} onChange={e => setSearch(e.target.value)}
              placeholder={`ค้นหา ${label}…`}
              style={{ width: "100%", padding: "6px 8px", border: "1px solid var(--border)", borderRadius: 6, fontSize: 12, background: "var(--bg-elevated)", color: "var(--ink)", boxSizing: "border-box" }} />
          </div>
          {/* Toolbar (Select all / Clear) */}
          <div style={{ display: "flex", gap: 8, padding: "6px 10px", borderBottom: "1px solid var(--border)", fontSize: 11 }}>
            <button onClick={selectAll}
              style={{ border: "none", background: "transparent", color: "var(--accent)", cursor: "pointer", padding: 0, fontWeight: 500 }}>
              เลือกทั้งหมด ({filtered.length})
            </button>
            <span className="muted small">·</span>
            <button onClick={clearAll}
              style={{ border: "none", background: "transparent", color: "var(--ink-faint)", cursor: "pointer", padding: 0, fontWeight: 500 }}>
              ล้าง ({selectedCount})
            </button>
            <span className="muted small" style={{ marginLeft: "auto" }}>{selectedCount}/{(options||[]).length}</span>
          </div>
          {/* Options */}
          <div style={{ maxHeight: 280, overflowY: "auto" }}>
            {filtered.length === 0 && (
              <div style={{ padding: "14px 12px", textAlign: "center", color: "var(--ink-faint)", fontSize: 12 }}>
                {search ? `ไม่พบ "${search}"` : "ไม่มีตัวเลือก"}
              </div>
            )}
            {filtered.map(o => {
              const checked = selectedSet.has(o.id);
              return (
                <label key={o.id} style={{
                  display: "flex", alignItems: "center", gap: 8,
                  padding: "7px 12px", cursor: "pointer", fontSize: 12,
                  background: checked ? "var(--row-hover)" : "transparent",
                }} onMouseDown={e => e.preventDefault()}>
                  <input type="checkbox" checked={checked} onChange={() => toggle(o.id)}
                    style={{ accentColor: "var(--accent)", width: 13, height: 13, cursor: "pointer" }} />
                  <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={o.name}>
                    {o.name}
                  </span>
                </label>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
};

/* ── DateRangePicker ─────────────────────────────────────────
   คลิก 1 = start, คลิก 2 = end  (auto-swap ถ้า end < start)
   hover = preview highlight ระหว่าง start กับ cursor
   แสดง 2 เดือนพร้อมกัน
──────────────────────────────────────────────────────────── */
const DateRangePicker = ({ since, until, onChange, onClose, inline = false }) => {
  const today = new Date(); today.setHours(0,0,0,0);
  // ใช้ local date (กัน timezone bug — toISOString จะ shift ไปก่อน 7 ชม.)
  const toStr  = d => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
  const fromStr = s => { const d = new Date(s + 'T00:00:00'); d.setHours(0,0,0,0); return d; };

  // viewMonth = first of the two calendar months
  const [viewYear,  setViewYear]  = React.useState(() => {
    const d = since ? fromStr(since) : new Date(today);
    d.setDate(1); return d.getFullYear();
  });
  const [viewMonth, setViewMonth] = React.useState(() => {
    const d = since ? fromStr(since) : new Date(today);
    return d.getMonth(); // 0-11
  });
  const [picking, setPicking]   = React.useState(since ? (until ? null : "end") : "start");
  const [hover, setHover]       = React.useState(null); // Date|null
  const [startDate, setStartDate] = React.useState(since ? fromStr(since) : null);
  const [endDate,   setEndDate]   = React.useState(until ? fromStr(until) : null);

  const prevMonth = () => {
    if (viewMonth === 0) { setViewMonth(11); setViewYear(y => y - 1); }
    else setViewMonth(m => m - 1);
  };
  const nextMonth = () => {
    if (viewMonth === 11) { setViewMonth(0); setViewYear(y => y + 1); }
    else setViewMonth(m => m + 1);
  };

  const month2Year  = viewMonth === 11 ? viewYear + 1 : viewYear;
  const month2Month = viewMonth === 11 ? 0 : viewMonth + 1;

  const handleDay = (d) => {
    if (d > today) return; // ห้ามเลือกอนาคต
    // ถ้า range ครบแล้ว (picking=null) หรือยังไม่มี start → เริ่มใหม่
    if (!startDate || picking === "start" || picking === null) {
      setStartDate(d); setEndDate(null); setPicking("end");
    } else {
      // กำลังเลือก end — swap อัตโนมัติถ้า end < start
      const [s, e] = d < startDate ? [d, startDate] : [startDate, d];
      setStartDate(s); setEndDate(e); setPicking(null);
      // ไม่ apply ทันที — รอให้กด "ยืนยัน"
    }
  };

  const yesterday = new Date(today - 86400000);
  const PRESETS = [
    { l: "วันนี้",       s: today,                                    e: today },
    { l: "เมื่อวาน",    s: yesterday,                                e: yesterday },
    // FB convention — "7 วันที่ผ่านมา" / "30 วัน" นับจากเมื่อวานย้อนกลับ ไม่รวมวันนี้
    { l: "7 วันที่ผ่านมา", s: new Date(today - 7*86400000),          e: yesterday },
    { l: "30 วัน",      s: new Date(today - 30*86400000),            e: yesterday },
    { l: "เดือนนี้",    s: new Date(today.getFullYear(), today.getMonth(), 1), e: today },
    { l: "เดือนที่แล้ว", s: (() => { const d=new Date(today); d.setDate(1); d.setMonth(d.getMonth()-1); return d; })(),
                         e: (() => { const d=new Date(today); d.setDate(0); return d; })() },
  ];

  const THdays = ["อา","จ","อ","พ","พฤ","ศ","ส"];
  const TH_MONTHS = ["ม.ค.","ก.พ.","มี.ค.","เม.ย.","พ.ค.","มิ.ย.","ก.ค.","ส.ค.","ก.ย.","ต.ค.","พ.ย.","ธ.ค."];

  const renderMonth = (year, month) => {
    const first = new Date(year, month, 1);
    const last  = new Date(year, month + 1, 0);
    const startDow = first.getDay(); // 0=Sun
    const days = [];
    for (let i = 0; i < startDow; i++) days.push(null);
    for (let d = 1; d <= last.getDate(); d++) days.push(new Date(year, month, d));

    return (
      <div style={{ minWidth: 220 }}>
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 10, fontWeight: 600, fontSize: 13 }}>
          <span>{TH_MONTHS[month]} {year + 543}</span>
        </div>
        <div style={{ display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: 1, textAlign: "center" }}>
          {THdays.map(d => <div key={d} style={{ fontSize: 10, color: "var(--ink-faint)", padding: "2px 0", fontWeight: 600 }}>{d}</div>)}
          {days.map((d, i) => {
            if (!d) return <div key={`e${i}`} />;
            const dStr      = toStr(d);
            const isFuture  = d > today;
            const hoverDate = (picking === "end" && !endDate) ? hover : null;
            const rangeEnd  = endDate || hoverDate || null;
            const isStart   = startDate && dStr === toStr(startDate);
            const isEnd     = endDate && dStr === toStr(endDate);
            const isHoverEdge = !endDate && hoverDate && dStr === toStr(hoverDate);
            // inRange: d is strictly between startDate and rangeEnd, direction-independent
            const t = d.getTime(), ts = startDate ? startDate.getTime() : null, te = rangeEnd ? rangeEnd.getTime() : null;
            const inRange = ts !== null && te !== null && ts !== te &&
              ((t > ts && t < te) || (t > te && t < ts));
            const isToday   = dStr === toStr(today);
            return (
              <div key={dStr}
                onMouseEnter={() => picking === "end" && setHover(d)}
                onMouseLeave={() => picking === "end" && setHover(null)}
                onClick={() => !isFuture && handleDay(d)}
                style={{
                  padding: "5px 2px", borderRadius: 6, cursor: isFuture ? "default" : "pointer",
                  fontSize: 12,
                  background: (isStart || isEnd || isHoverEdge) ? "var(--accent)"
                            : inRange ? "var(--accent-bg, #eef2ff)"
                            : "transparent",
                  color: (isStart || isEnd || isHoverEdge) ? "#fff"
                       : isFuture ? "var(--ink-faint)"
                       : isToday ? "var(--accent)"
                       : "var(--ink)",
                  fontWeight: (isStart || isEnd || isHoverEdge || isToday) ? 700 : 400,
                  outline: isToday && !(isStart||isEnd||isHoverEdge) ? "1px solid var(--accent)" : "none",
                  opacity: isFuture ? 0.35 : 1,
                }}>
                {d.getDate()}
              </div>
            );
          })}
        </div>
      </div>
    );
  };

  const sinceLabel = startDate ? startDate.toLocaleDateString("th-TH", { day: "numeric", month: "short" }) : "วันเริ่ม";
  const untilLabel = endDate   ? endDate.toLocaleDateString("th-TH",   { day: "numeric", month: "short" }) : "วันสิ้นสุด";

  return (
    <div style={{
      ...(inline
        ? { position: "static" }
        : { position: "absolute", top: "calc(100% + 6px)", right: 0, zIndex: 500 }),
      background: "var(--bg)", border: "1px solid var(--border)", borderRadius: 12,
      boxShadow: "0 8px 32px rgba(0,0,0,.15)", display: "flex", overflow: "hidden",
    }} onClick={e => e.stopPropagation()}>

      {/* Presets */}
      <div style={{ width: 130, borderRight: "1px solid var(--border)", padding: "12px 0" }}>
        <div style={{ fontSize: 10, fontWeight: 700, color: "var(--ink-faint)", textTransform: "uppercase", letterSpacing: ".06em", padding: "0 14px 6px" }}>Quick Select</div>
        {PRESETS.map(p => {
          const active = startDate && endDate && toStr(startDate) === toStr(p.s) && toStr(endDate) === toStr(p.e);
          return (
            <div key={p.l} onClick={() => { setStartDate(p.s); setEndDate(p.e); setPicking(null); onChange(toStr(p.s), toStr(p.e)); }}
              style={{ padding: "7px 14px", cursor: "pointer", fontSize: 12, fontWeight: active ? 600 : 400,
                       color: active ? "var(--accent)" : "var(--ink)",
                       background: active ? "var(--accent-bg, #eef2ff)" : "transparent" }}>
              {p.l}
            </div>
          );
        })}
      </div>

      {/* Calendar */}
      <div style={{ padding: "14px 16px" }}>
        {/* Nav */}
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 12 }}>
          <button onClick={prevMonth} style={{ background: "none", border: "1px solid var(--border)", borderRadius: 6, width: 28, height: 28, cursor: "pointer", color: "var(--ink)", display: "flex", alignItems: "center", justifyContent: "center" }}>‹</button>
          <div style={{ fontSize: 12, color: "var(--ink-soft)", display: "flex", gap: 6 }}>
            {picking === "start" && <span style={{ color: "var(--accent)", fontWeight: 600 }}>เลือกวันเริ่ม</span>}
            {picking === "end"   && <span style={{ color: "var(--accent)", fontWeight: 600 }}>เลือกวันสิ้นสุด</span>}
            {!picking && startDate && endDate && (
              <span style={{ fontWeight: 600 }}>{sinceLabel} – {untilLabel}</span>
            )}
          </div>
          <button onClick={nextMonth} style={{ background: "none", border: "1px solid var(--border)", borderRadius: 6, width: 28, height: 28, cursor: "pointer", color: "var(--ink)", display: "flex", alignItems: "center", justifyContent: "center" }}>›</button>
        </div>

        {/* Two months */}
        <div style={{ display: "flex", gap: 24 }}>
          {renderMonth(viewYear, viewMonth)}
          {renderMonth(month2Year, month2Month)}
        </div>

        {/* Footer */}
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 14, paddingTop: 12, borderTop: "1px solid var(--border)" }}>
          <button onClick={() => { setStartDate(null); setEndDate(null); setPicking("start"); onChange("", ""); }}
            style={{ fontSize: 12, color: "var(--ink-faint)", background: "none", border: "none", cursor: "pointer" }}>ล้าง</button>
          <div style={{ display: "flex", gap: 8 }}>
            <button onClick={onClose} style={{ padding: "5px 14px", border: "1px solid var(--border)", borderRadius: 7, background: "transparent", cursor: "pointer", fontSize: 12 }}>ยกเลิก</button>
            <button onClick={() => { onChange(toStr(startDate), toStr(endDate)); onClose(); }} disabled={!endDate}
              style={{ padding: "5px 14px", border: "none", borderRadius: 7, background: "var(--accent)", color: "#fff", cursor: endDate ? "pointer" : "not-allowed", opacity: endDate ? 1 : 0.5, fontSize: 12, fontWeight: 600 }}>
              ยืนยัน
            </button>
          </div>
        </div>
      </div>
    </div>
  );
};

// Expose to app.jsx (load order: screens-1 → app)
window.DateRangePicker = DateRangePicker;


const fmt = {
  money: (n) => "฿" + Math.round(n).toLocaleString("en-US"),
  moneyK: (n) => n >= 1000 ? "฿" + (n / 1000).toFixed(1) + "k" : "฿" + Math.round(n),
  num: (n) => Math.round(n).toLocaleString("en-US"),
  pct: (n, d = 2) => n.toFixed(d) + "%",
  delta: (n) => (n > 0 ? "+" : "") + n.toFixed(1) + "%",
};

const Sparkline = ({ data, color = "var(--accent)", width = 96, height = 28, yMin, yMax, refLine }) => {
  if (!data || data.length === 0) return null;
  const dataMax = Math.max(...data, 1);
  const dataMin = Math.min(...data, 0);
  // ถ้า yMin/yMax กำหนดมา ใช้เป็น default; แต่ถ้า data เกิน → ขยายตาม data
  const min = yMin !== undefined ? Math.min(yMin, dataMin) : dataMin;
  const max = yMax !== undefined ? Math.max(yMax, dataMax) : dataMax;
  const range = max - min || 1;
  const step = width / (data.length - 1 || 1);
  const yOf = (v) => height - ((v - min) / range) * (height - 4) - 2;
  const points = data.map((v, i) => `${i * step},${yOf(v)}`).join(" ");
  return (
    <svg width={width} height={height} style={{ display: "block" }}>
      {/* Optional reference line — เช่น ROAS = 1 (break-even) */}
      {refLine !== undefined && refLine >= min && refLine <= max && (
        <line x1={0} x2={width} y1={yOf(refLine)} y2={yOf(refLine)}
              stroke="var(--ink-faint)" strokeWidth="0.5" strokeDasharray="2,2" opacity="0.5" />
      )}
      <polyline fill={color} fillOpacity="0.08" stroke="none" points={`0,${height} ${points} ${width},${height}`} />
      <polyline fill="none" stroke={color} strokeWidth="1.5" points={points} />
    </svg>
  );
};

const KpiCard = ({ label, value, delta, sublabel, icon, trend, color }) => (
  <div className="kpi-card">
    <div className="kpi-head">
      <span className="kpi-label">{label}</span>
      {icon && <span className="kpi-icon">{icon}</span>}
    </div>
    <div className="kpi-value">{value}</div>
    <div className="kpi-foot">
      {delta != null && (
        <span className={`kpi-delta ${delta >= 0 ? "up" : "down"}`}>
          <svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor">
            <path d={delta >= 0 ? "M12 4L20 14L4 14Z" : "M12 20L4 10L20 10Z"}/>
          </svg>
          {Math.abs(delta).toFixed(1)}%
        </span>
      )}
      {sublabel && <span className="kpi-sub">{sublabel}</span>}
      {trend && <span style={{ marginLeft: "auto" }}><Sparkline data={trend} color={color || "var(--accent)"} width={64} height={20} /></span>}
    </div>
  </div>
);

const StatusBadge = ({ status }) => {
  const map = {
    ACTIVE:   { bg: "var(--success-bg)", fg: "var(--success)", dot: "var(--success)", label: "Active" },
    PAUSED:   { bg: "var(--neutral-bg)", fg: "var(--ink-soft)", dot: "var(--ink-faint)", label: "Paused" },
    LEARNING: { bg: "var(--accent-bg)",  fg: "var(--accent)",   dot: "var(--accent)",   label: "Learning" },
    ARCHIVED: { bg: "#f4f4f5",            fg: "#71717a",          dot: "#71717a",         label: "Archived" },
    STALE:    { bg: "#fef3c7",            fg: "#92400e",          dot: "#d97706",         label: "🕐 Stale", title: "ค้างเกิน 60 วันไม่มี delivery — อาจถูกลบจาก FB" },
    DELETED:  { bg: "#fee2e2",            fg: "#991b1b",          dot: "#dc2626",         label: "Deleted" },
  };
  const s = map[status] || map.PAUSED;
  return (
    <span className="status-badge" style={{ background: s.bg, color: s.fg }} title={s.title || s.label}>
      <span className="status-dot" style={{ background: s.dot }}></span>
      {s.label}
    </span>
  );
};

const AiStatusChip = ({ status }) => {
  const map = {
    OPTIMIZED: { c: "var(--success)", l: "Optimized" },
    WATCHING: { c: "var(--warning)", l: "Watching" },
    AT_RISK: { c: "var(--danger)", l: "At risk" },
    SCALING: { c: "var(--accent)", l: "Scaling" },
    PAUSED_BY_AI: { c: "var(--ink-faint)", l: "Paused by AI" },
  };
  const s = map[status] || { c: "var(--ink-faint)", l: status };
  return (
    <span className="ai-chip" style={{ color: s.c, borderColor: s.c + "33" }}>
      <svg width="9" height="9" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L13.5 8.5L20 10L13.5 11.5L12 18L10.5 11.5L4 10L10.5 8.5Z"/></svg>
      {s.l}
    </span>
  );
};

/* ================ DASHBOARD ================ */
const Dashboard = ({ timeRange, setTimeRange, adAccountId, onSelectCampaign, onSelectAd, onOpenBuilder }) => {
  // ── Live stats จาก CampaignsHierarchy (เปลี่ยนตาม filter/select) ──
  const [rawStats, setStats] = React.useState(null);
  const [loading, setLoading] = React.useState(true);

  // ── Spend % alternation (spend/revenue vs spend/(revenue+pending)) ──
  const [spendPctMode, setSpendPctMode] = React.useState(0);  // 0 = confirmed, 1 = incl. pending
  React.useEffect(() => {
    const id = setInterval(() => setSpendPctMode(m => (m + 1) % 2), 5000);
    return () => clearInterval(id);
  }, []);

  // ── Export dropdown ──
  const [exportOpen, setExportOpen] = React.useState(false);
  const [exporting, setExporting] = React.useState(false);
  const exportMenuRef = React.useRef(null);
  React.useEffect(() => {
    if (!exportOpen) return;
    const close = (e) => { if (!exportMenuRef.current?.contains(e.target)) setExportOpen(false); };
    document.addEventListener("mousedown", close);
    return () => document.removeEventListener("mousedown", close);
  }, [exportOpen]);

  // ── Data source toggle: Meta (FB pixel) vs Real (our orders) ──
  const [dataSource, setDataSource] = React.useState(() => Urlp.get("src") === "real" ? "real" : "meta");
  React.useEffect(() => { Urlp.patch({ src: dataSource === "meta" ? null : dataSource }); }, [dataSource]);

  // ── Shop Type filter (multi-select — array จาก orders.shop_type) ────
  const [shopTypes, setShopTypes] = React.useState(() => {
    const raw = Urlp.get("shop") || "";
    return raw ? raw.split(",").filter(Boolean) : [];   // [] = ทุก shop type
  });
  const [shopTypeOptions, setShopTypeOptions] = React.useState([]);
  const [shopMenuOpen, setShopMenuOpen] = React.useState(false);
  React.useEffect(() => { Urlp.patch({ shop: shopTypes.length ? shopTypes.join(",") : null }); }, [shopTypes]);
  React.useEffect(() => {
    if (!window.DB?.getShopTypes) return;
    window.DB.getShopTypes().then(setShopTypeOptions).catch(() => setShopTypeOptions([]));
  }, []);
  const shopTypeKey = shopTypes.join(",");
  const toggleShopType = (st) => {
    setShopTypes(prev => prev.includes(st) ? prev.filter(x => x !== st) : [...prev, st]);
  };
  // shop_type → กรอง Campaigns table (ad_ids ที่ปรากฏใน orders ของ shop_type นี้)
  const [shopFilter, setShopFilter] = React.useState(null);  // null = ไม่ filter; { adIds, adsetIds, campaignIds }
  // shop_type orders aggregate — override KPI Revenue/Orders ใน Real Order mode
  const [shopOrdersAgg, setShopOrdersAgg] = React.useState(null);

  // ── Custom date range — sync กับ URL ─────────────────────
  // (ย้ายขึ้นมาก่อน shopFilterRange เพราะ useMemo ด้านล่างใช้ state เหล่านี้)
  const initSince   = Urlp.get("since")   || "";
  const initUntil   = Urlp.get("until")   || "";
  const initApplied = !!(initSince && initUntil);
  const [customSince,   setCustomSince]   = React.useState(initSince);
  const [customUntil,   setCustomUntil]   = React.useState(initUntil);
  const [customApplied, setCustomApplied] = React.useState(initApplied);
  const [showCustom,    setShowCustom]    = React.useState(false);

  // resolve date range — sync กับ Dashboard preset logic
  const shopFilterRange = React.useMemo(() => {
    if (customApplied && customSince && customUntil) return { since: customSince, until: customUntil };
    const today = new Date(); today.setHours(0,0,0,0);
    const ymd = (d) => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`;
    if (timeRange === "today") return { since: ymd(today), until: ymd(today) };
    if (timeRange === "yesterday") {
      const y = new Date(today); y.setDate(y.getDate() - 1);
      return { since: ymd(y), until: ymd(y) };
    }
    const m = { "7d": 7, "14d": 14, "30d": 30, "6m": 180, "1y": 365 };
    const n = m[timeRange] ?? 7;
    const s = new Date(today); s.setDate(s.getDate() - n);
    return { since: ymd(s), until: ymd(today) };
  }, [timeRange, customApplied, customSince, customUntil]);
  React.useEffect(() => {
    if (!shopTypes.length || !window.DB?.getAdIdsByShopType) {
      setShopFilter(null);
      return;
    }
    let cancelled = false;
    (async () => {
      try {
        // ไม่ส่ง date — query ทุก order ที่มี shop_type นี้ (filter "ขายอะไร" ไม่ใช่ "เมื่อไหร่")
        const adIds = await window.DB.getAdIdsByShopType({ shopTypes });
        if (!adIds.length) { if (!cancelled) setShopFilter({ adIds: [], adsetIds: [], campaignIds: [] }); return; }
        const h = await window.DB.resolveAdHierarchy(adIds);
        if (!cancelled) setShopFilter(h);
      } catch (e) {
        if (!cancelled) setShopFilter({ adIds: [], adsetIds: [], campaignIds: [] });
      }
    })();
    return () => { cancelled = true; };
  }, [shopTypeKey, adAccountId]);

  // ── Orders aggregate ใน period (สำหรับ Real Order KPI override) ─
  //    Trigger ทั้งกรณี shop_type filter หรือ scope (campaign/adset/ad) selected
  const scopeForAgg = rawStats?.scope || "all";
  const scopeIdsForAgg = rawStats?.scopeIds || null;
  const scopeIdsKey = scopeIdsForAgg ? scopeIdsForAgg.join(",") : "";
  // Always fetch orders aggregate in Real Order mode — KPI = Organic + Attributed (consistent)
  const aggShouldFetch = dataSource === "real";
  React.useEffect(() => {
    if (!aggShouldFetch || !window.DB?.getOrdersAggregate) {
      setShopOrdersAgg(null);
      return;
    }
    let cancelled = false;
    window.DB.getOrdersAggregate({
      since: shopFilterRange.since, until: shopFilterRange.until,
      shopTypes: shopTypes.length ? shopTypes : null,
      adAccountId,
      scope: scopeForAgg,
      scopeIds: scopeIdsForAgg,
    })
      .then(agg => { if (!cancelled) setShopOrdersAgg(agg); })
      .catch(() => { if (!cancelled) setShopOrdersAgg(null); });
    return () => { cancelled = true; };
  }, [dataSource, shopTypeKey, shopFilterRange.since, shopFilterRange.until, adAccountId, scopeForAgg, scopeIdsKey, aggShouldFetch]);

  // ── Final display stats — override Real Order fields เมื่อมี shop filter ──
  //    เรียก `stats` (อยู่ใน JSX) เพื่อความสะดวก
  const stats = React.useMemo(() => {
    if (!rawStats) return null;
    if (dataSource !== "real" || !shopOrdersAgg) return rawStats;
    // override revenue/orders/pending/customers ด้วย orders aggregate (รวม organic)
    return {
      ...rawStats,
      revenue:         shopOrdersAgg.confirmed.revenue,
      orders:          shopOrdersAgg.confirmed.orders,
      pendingRevenue:  shopOrdersAgg.pending.revenue,
      pendingOrders:   shopOrdersAgg.pending.orders,
      cancelledOrders: shopOrdersAgg.cancelled.orders,
      cancelledRevenue: shopOrdersAgg.cancelled.revenue,
      aov:             shopOrdersAgg.aov,
      // ROAS recompute (Real revenue / Spend)
      roas:            rawStats.spend > 0 ? shopOrdersAgg.confirmed.revenue / rawStats.spend : 0,
    };
  }, [rawStats, dataSource, shopOrdersAgg]);

  // sync date range → URL
  React.useEffect(() => {
    Urlp.patch({
      range: customApplied ? null : timeRange,  // ถ้า applied custom, ไม่ต้องมี range
      since: customApplied ? customSince : null,
      until: customApplied ? customUntil : null,
    });
  }, [timeRange, customApplied, customSince, customUntil]);

  const applyCustom = (s, e) => {
    setCustomSince(s); setCustomUntil(e);
    setCustomApplied(!!(s && e));
    setShowCustom(false);
  };
  const clearCustom = () => {
    setCustomSince(""); setCustomUntil("");
    setCustomApplied(false); setShowCustom(false);
  };

  // ── Export helpers ──────────────────────────────────────
  // Lazy-load SheetJS once per session
  const loadXLSX = () => new Promise((resolve, reject) => {
    if (window.XLSX) return resolve(window.XLSX);
    const s = document.createElement("script");
    s.src = "https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js";
    s.onload = () => resolve(window.XLSX);
    s.onerror = () => reject(new Error("Failed to load XLSX library"));
    document.head.appendChild(s);
  });
  // Active hierarchy tab (campaign/adset/ad) — read from URL like CampaignsHierarchy
  const activeLevel = () => {
    const t = (typeof Urlp !== "undefined") ? Urlp.get("tab") : null;
    return ["campaign", "adset", "ad"].includes(t) ? t : "campaign";
  };
  // Resolve date opts for DB calls (matches CampaignsHierarchy logic)
  const resolveDateOpts = () => {
    const { since, until } = shopFilterRange;
    if (customApplied && customSince && customUntil) return { since: customSince, until: customUntil };
    if (timeRange === "today" || timeRange === "yesterday") return { since, until };
    return { days: ({ "7d": 7, "14d": 14, "30d": 30, "6m": 180, "1y": 365 })[timeRange] ?? 7 };
  };
  // Fetch entities for the active level (respects shopFilter)
  // Export passes includeDeleted=true so orphan-attributed orders land in a
  // labeled entity row instead of "Other attributed".
  const fetchEntitiesForLevel = async (level, { includeDeleted = true } = {}) => {
    const dateOpts = { ...resolveDateOpts(), includeDeleted };
    if (level === "campaign") {
      let cs = await window.DB.getCampaigns(adAccountId || null, dateOpts);
      if (!Array.isArray(cs)) cs = [];
      if (shopFilter?.campaignIds) { const a = new Set(shopFilter.campaignIds); cs = cs.filter(c => a.has(c.id)); }
      return cs;
    }
    // adset/ad: need full chain
    let cs = await window.DB.getCampaigns(adAccountId || null, dateOpts);
    if (!Array.isArray(cs)) cs = [];
    if (shopFilter?.campaignIds) { const a = new Set(shopFilter.campaignIds); cs = cs.filter(c => a.has(c.id)); }
    const campIds = cs.map(c => c.id);
    if (!campIds.length) return [];
    let sets = await window.DB.getAdSets(campIds, dateOpts);
    if (!Array.isArray(sets)) sets = [];
    if (shopFilter?.adsetIds) { const a = new Set(shopFilter.adsetIds); sets = sets.filter(s => a.has(s.id)); }
    if (level === "adset") return sets;
    const setIds = sets.map(s => s.id);
    if (!setIds.length) return [];
    let ads = await window.DB.getAds(setIds, dateOpts);
    if (!Array.isArray(ads)) ads = [];
    if (shopFilter?.adIds) { const a = new Set(shopFilter.adIds); ads = ads.filter(d => a.has(d.id)); }
    return ads;
  };
  // Per-entity KPI dict
  const kpiOf = (e) => {
    const revKey = dataSource === "meta" ? "revenueMeta" : "revenuePeriod";
    const ordKey = dataSource === "meta" ? "ordersMeta"  : "ordersPeriod";
    const spend = e.spendPeriod || 0, rev = e[revKey] || 0, ord = e[ordKey] || 0;
    const imp = e.impressionsPeriod || 0, clk = e.clicksPeriod || 0;
    return {
      spend, revenue: rev, orders: ord, impressions: imp, reach: e.reachPeriod || 0, clicks: clk,
      messages: e.messagesPeriod || 0,
      roas: spend > 0 ? rev / spend : 0,
      ctr:  imp > 0 ? (clk / imp) * 100 : 0,
      cpc:  clk > 0 ? spend / clk : 0,
      cpm:  imp > 0 ? (spend / imp) * 1000 : 0,
      aov:  ord > 0 ? rev / ord : 0,
      cpa:  ord > 0 ? spend / ord : 0,
      pending_revenue:   e.pendingRevenue   || 0,
      pending_orders:    e.pendingOrders    || 0,
      cancelled_revenue: e.cancelledRevenue || 0,
      cancelled_orders:  e.cancelledOrders  || 0,
    };
  };

  // Per-entity Customer Segment breakdown (both modes) — respects active level
  const fetchSegmentBd = async (level, entityId, since, until) => {
    const args = { since, until, adAccountId, scope: level, scopeIds: [entityId], shopTypes: shopTypes.length ? shopTypes : null };
    const [orderBd, custBd] = await Promise.all([
      window.DB.getCustomerBreakdown?.({ ...args, mode: "orders"    }).catch(() => null),
      window.DB.getCustomerBreakdown?.({ ...args, mode: "customers" }).catch(() => null),
    ]);
    return { orderBd, custBd };
  };
  const triggerDownload = (blob, filename) => {
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url; a.download = filename;
    document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  };

  // ── Export: JSON ──────────────────────────────────────
  const exportJSON = async () => {
    if (exporting) return;
    setExporting(true);
    setExportOpen(false);
    try {
      if (!window.DB?.getCampaigns) throw new Error("DB unavailable");
      const level = activeLevel();
      const { since, until } = shopFilterRange;
      const entities = await fetchEntitiesForLevel(level);
      const rows = await Promise.all(entities.map(async (e) => {
        const { orderBd, custBd } = await fetchSegmentBd(level, e.id, since, until);
        return {
          [`${level}_id`]:   e.id,
          [`${level}_name`]: e.name,
          status:    e.status || e.effective_status || null,
          objective: e.objective || null,
          kpi:       kpiOf(e),
          customer_segment_orders:    orderBd ? { matrix: orderBd.matrix, source: orderBd.source, totals: orderBd.totals } : null,
          customer_segment_customers: custBd  ? { matrix: custBd.matrix,  source: custBd.source,  totals: custBd.totals  } : null,
        };
      }));
      // Top-level totals — matches Overview KPI rule:
      //   All accounts → include organic
      //   Specific account or drill scope → exclude organic
      const totalsAgg = await window.DB.getOrdersAggregate?.({
        since, until,
        shopTypes: shopTypes.length ? shopTypes : null,
        adAccountId,
        scope: "all",
      }).catch(() => null);
      const payload = {
        exported_at: new Date().toISOString(),
        level,
        date_range: { since, until },
        ad_account_id: adAccountId || null,
        shop_types: shopTypes,
        data_source: dataSource,
        totals: totalsAgg ? {
          confirmed:  totalsAgg.confirmed,
          pending:    totalsAgg.pending,
          cancelled:  totalsAgg.cancelled,
          attributed: totalsAgg.attributed,
          organic:    totalsAgg.organic,
          total_orders: totalsAgg.totalOrders,
          customers:    totalsAgg.customers,
          aov:          totalsAgg.aov,
          includes_organic: !adAccountId,   // matches Overview rule
        } : null,
        [`${level}s`]: rows,
      };
      const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
      triggerDownload(blob, `ads-${level}-${since}_to_${until}.json`);
    } catch (e) {
      alert("Export ล้มเหลว: " + (e.message || e));
    } finally {
      setExporting(false);
    }
  };

  // ── Export: Excel (3 sheets — Summary / Daily / Meta) ──
  const exportXLSX = async () => {
    if (exporting) return;
    setExporting(true);
    setExportOpen(false);
    try {
      if (!window.DB?.getCampaigns) throw new Error("DB unavailable");
      const XLSX = await loadXLSX();
      const level = activeLevel();
      const { since, until } = shopFilterRange;
      const entities = await fetchEntitiesForLevel(level);

      // Per-entity segment breakdown
      const breakdowns = await Promise.all(entities.map(e => fetchSegmentBd(level, e.id, since, until)));

      // Always recompute revenue/orders/pending/cancelled per entity from
      // orders aggregate (live count). Insights rollup is approximate and may
      // be stale, and doesn't know shop_type. Customer Segment matrices use
      // the same live query — using orders aggregate here keeps both blocks
      // in agreement for every row.
      const ordersAggByEntity = await Promise.all(entities.map(e => window.DB.getOrdersAggregate?.({
        since, until,
        shopTypes: shopTypes.length ? shopTypes : null,
        adAccountId,
        scope: level,
        scopeIds: [e.id],
      }).catch(() => null)));

      // Top-level totals — same rule as Overview KPI:
      //   All accounts → include organic
      //   Specific account → exclude organic
      const totalsAgg = await window.DB.getOrdersAggregate?.({
        since, until,
        shopTypes: shopTypes.length ? shopTypes : null,
        adAccountId,
        scope: "all",
      }).catch(() => null);

      const SEGS = ["new", "loyal", "returning", "dormant"];
      const STATUSES = ["pending", "confirmed", "cancelled"];

      // Sheet 1: Summary — 1 row per entity, flattened
      const summaryRows = entities.map((e, i) => {
        const k = kpiOf(e);
        const { orderBd, custBd } = breakdowns[i];
        // Always override revenue/orders/pending/cancelled from live orders aggregate
        const agg = ordersAggByEntity[i];
        if (agg) {
          k.revenue            = agg.confirmed.revenue;
          k.orders             = agg.confirmed.orders;
          k.pending_revenue    = agg.pending.revenue;
          k.pending_orders     = agg.pending.orders;
          k.cancelled_revenue  = agg.cancelled.revenue;
          k.cancelled_orders   = agg.cancelled.orders;
          k.roas = k.spend > 0 ? k.revenue / k.spend : 0;
          k.aov  = k.orders > 0 ? k.revenue / k.orders : 0;
          k.cpa  = k.orders > 0 ? k.spend / k.orders : 0;
        }
        const row = {
          [`${level}_id`]:   e.id,
          [`${level}_name`]: e.name,
          status:    e.status || e.effective_status || "",
          objective: e.objective || "",
          spend: k.spend, revenue: k.revenue, orders: k.orders,
          impressions: k.impressions, reach: k.reach, clicks: k.clicks,
          messages: k.messages,
          ROAS: +(k.roas.toFixed(4)), CTR: +(k.ctr.toFixed(4)),
          CPC: +(k.cpc.toFixed(2)), CPM: +(k.cpm.toFixed(2)),
          AOV: +(k.aov.toFixed(2)), CPA: +(k.cpa.toFixed(2)),
          pending_revenue:   k.pending_revenue,
          pending_orders:    k.pending_orders,
          cancelled_revenue: k.cancelled_revenue,
          cancelled_orders:  k.cancelled_orders,
        };
        // Flatten segment matrices (orders mode + customers mode)
        for (const seg of SEGS) for (const st of STATUSES) {
          const omCell = orderBd?.matrix?.[seg]?.[st] || { count: 0, revenue: 0 };
          row[`SegOrd_${seg}_${st}_count`]   = omCell.count;
          row[`SegOrd_${seg}_${st}_revenue`] = omCell.revenue;
          const cmCell = custBd?.matrix?.[seg]?.[st] || { count: 0, revenue: 0 };
          row[`SegCust_${seg}_${st}_count`]   = cmCell.count;
          row[`SegCust_${seg}_${st}_revenue`] = cmCell.revenue;
        }
        // Source split (from orders mode) — Organic = no ad_id, Attributed = has ad_id, Cancelled = canceled
        for (const src of ["organic", "attributed", "cancelled"]) {
          const cell = orderBd?.source?.[src] || { count: 0, revenue: 0 };
          row[`Src_${src}_count`]   = cell.count;
          row[`Src_${src}_revenue`] = cell.revenue;
        }
        return row;
      });

      // Append summary rows in order: [entities…] → [ORGANIC] → [TOTAL]
      // Organic row appears only in All-accounts view (organic has no ad_account).
      if (summaryRows.length && totalsAgg) {
        const blankRow = () => {
          const r = { [`${level}_id`]: "", [`${level}_name`]: "", status: "", objective: "",
            spend: 0, revenue: 0, orders: 0, impressions: 0, reach: 0, clicks: 0, messages: 0,
            ROAS: 0, CTR: 0, CPC: 0, CPM: 0, AOV: 0, CPA: 0,
            pending_revenue: 0, pending_orders: 0, cancelled_revenue: 0, cancelled_orders: 0 };
          for (const seg of SEGS) for (const st of STATUSES) {
            r[`SegOrd_${seg}_${st}_count`]    = "";
            r[`SegOrd_${seg}_${st}_revenue`]  = "";
            r[`SegCust_${seg}_${st}_count`]   = "";
            r[`SegCust_${seg}_${st}_revenue`] = "";
          }
          r["Src_organic_count"] = ""; r["Src_organic_revenue"] = "";
          r["Src_attributed_count"] = ""; r["Src_attributed_revenue"] = "";
          r["Src_cancelled_count"] = ""; r["Src_cancelled_revenue"] = "";
          return r;
        };

        // ORGANIC row (only when All accounts — organic has no ad_account)
        if (!adAccountId && totalsAgg.organic) {
          const org = blankRow();
          org[`${level}_name`] = "🔵 ORGANIC (no ad_id)";
          org.revenue          = totalsAgg.organic.revenue;
          org.orders           = totalsAgg.organic.orders;
          org.AOV              = totalsAgg.organic.orders > 0
            ? +((totalsAgg.organic.revenue / totalsAgg.organic.orders).toFixed(2))
            : 0;
          org["Src_organic_count"]   = totalsAgg.organic.orders;
          org["Src_organic_revenue"] = totalsAgg.organic.revenue;
          summaryRows.push(org);
        }

        // "Other attributed" row — orders attributed to campaigns NOT shown
        // (deleted/filtered out) so TOTAL still equals sum of all visible rows.
        // Computed as totalsAgg.confirmed - (sum of entities) - organic.
        const sumKey = (key) => summaryRows.reduce((a, r) => a + (Number(r[key]) || 0), 0);
        const entityRev   = sumKey("revenue");
        const entityOrd   = sumKey("orders");
        const entityPndR  = sumKey("pending_revenue");
        const entityPndO  = sumKey("pending_orders");
        const entityCanR  = sumKey("cancelled_revenue");
        const entityCanO  = sumKey("cancelled_orders");
        const orgRev      = (!adAccountId && totalsAgg.organic) ? totalsAgg.organic.revenue : 0;
        const orgOrd      = (!adAccountId && totalsAgg.organic) ? totalsAgg.organic.orders  : 0;
        const otherRev = Math.max(0, totalsAgg.confirmed.revenue - entityRev - orgRev);
        const otherOrd = Math.max(0, totalsAgg.confirmed.orders  - entityOrd - orgOrd);
        const otherPndR = Math.max(0, totalsAgg.pending.revenue   - entityPndR);
        const otherPndO = Math.max(0, totalsAgg.pending.orders    - entityPndO);
        const otherCanR = Math.max(0, totalsAgg.cancelled.revenue - entityCanR);
        const otherCanO = Math.max(0, totalsAgg.cancelled.orders  - entityCanO);
        if (otherRev > 0 || otherOrd > 0 || otherPndO > 0 || otherCanO > 0) {
          const oth = blankRow();
          oth[`${level}_name`]   = "⚠️ Other attributed (deleted/filtered campaigns)";
          oth.revenue            = otherRev;
          oth.orders             = otherOrd;
          oth.AOV                = otherOrd > 0 ? +((otherRev / otherOrd).toFixed(2)) : 0;
          oth.pending_revenue    = otherPndR;
          oth.pending_orders     = otherPndO;
          oth.cancelled_revenue  = otherCanR;
          oth.cancelled_orders   = otherCanO;
          summaryRows.push(oth);
        }

        // TOTAL row — strict sum of all visible rows (entities + organic + other)
        // Guarantees: TOTAL.col === SUM(col across rows above)
        const totalSpend       = sumKey("spend");
        const totalRevenue     = sumKey("revenue");
        const totalOrders      = sumKey("orders");
        const totalImpressions = sumKey("impressions");
        const totalReach       = sumKey("reach");
        const totalClicks      = sumKey("clicks");
        const totalMessages    = sumKey("messages");
        const totalPndR        = sumKey("pending_revenue");
        const totalPndO        = sumKey("pending_orders");
        const totalCanR        = sumKey("cancelled_revenue");
        const totalCanO        = sumKey("cancelled_orders");
        const totalRow = blankRow();
        totalRow[`${level}_name`] = adAccountId
          ? "Σ TOTAL (excl. organic — account filtered)"
          : "Σ TOTAL (sum of rows above)";
        totalRow.spend       = totalSpend;
        totalRow.revenue     = totalRevenue;
        totalRow.orders      = totalOrders;
        totalRow.impressions = totalImpressions;
        totalRow.reach       = totalReach;
        totalRow.clicks      = totalClicks;
        totalRow.messages    = totalMessages;
        totalRow.ROAS = totalSpend > 0 ? +((totalRevenue / totalSpend).toFixed(4)) : 0;
        totalRow.CTR  = totalImpressions > 0 ? +(((totalClicks / totalImpressions) * 100).toFixed(4)) : 0;
        totalRow.CPC  = totalClicks > 0 ? +((totalSpend / totalClicks).toFixed(2)) : 0;
        totalRow.CPM  = totalImpressions > 0 ? +(((totalSpend / totalImpressions) * 1000).toFixed(2)) : 0;
        totalRow.AOV  = totalOrders > 0 ? +((totalRevenue / totalOrders).toFixed(2)) : 0;
        totalRow.CPA  = totalOrders > 0 ? +((totalSpend / totalOrders).toFixed(2)) : 0;
        totalRow.pending_revenue   = totalPndR;
        totalRow.pending_orders    = totalPndO;
        totalRow.cancelled_revenue = totalCanR;
        totalRow.cancelled_orders  = totalCanO;
        totalRow["Src_organic_count"]      = totalsAgg.organic.orders;
        totalRow["Src_organic_revenue"]    = totalsAgg.organic.revenue;
        totalRow["Src_attributed_count"]   = totalsAgg.attributed.orders;
        totalRow["Src_attributed_revenue"] = totalsAgg.attributed.revenue;
        totalRow["Src_cancelled_count"]    = totalsAgg.cancelled.orders;
        totalRow["Src_cancelled_revenue"]  = totalsAgg.cancelled.revenue;
        summaryRows.push(totalRow);
      }

      // Sheet 2: Daily — 1 row per (entity, day)
      const dailyRows = [];
      const revKey = dataSource === "meta" ? "trendDailyMetaRev" : "trendDailyOurRev";
      const ordKey = dataSource === "meta" ? "trendDailyMetaOrders" : "trendDailyOurOrders";
      for (const e of entities) {
        const dates = e.trendDates || [];
        for (let i = 0; i < dates.length; i++) {
          const sp = e.trendDailySpend?.[i] || 0;
          const rv = e[revKey]?.[i] || 0;
          const od = e[ordKey]?.[i] || 0;
          const im = e.trendDailyImpressions?.[i] || 0;
          const ck = e.trendDailyClicks?.[i] || 0;
          dailyRows.push({
            date: dates[i],
            [`${level}_name`]: e.name,
            [`${level}_id`]:   e.id,
            spend: sp, revenue: rv, orders: od,
            impressions: im, clicks: ck,
            ROAS: sp > 0 ? +((rv / sp).toFixed(4)) : 0,
            CTR:  im > 0 ? +(((ck / im) * 100).toFixed(4)) : 0,
            CPC:  ck > 0 ? +((sp / ck).toFixed(2)) : 0,
          });
        }
      }
      dailyRows.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));

      // Sheet 3: Meta
      const metaRows = [
        { key: "exported_at",    value: new Date().toISOString() },
        { key: "level",          value: level },
        { key: "since",          value: since },
        { key: "until",          value: until },
        { key: "ad_account_id",  value: adAccountId || "(all)" },
        { key: "data_source",    value: dataSource },
        { key: "shop_types",     value: shopTypes.length ? shopTypes.join(",") : "(all)" },
        { key: "entity_count",   value: entities.length },
        { key: "daily_rows",     value: dailyRows.length },
      ];

      const wb = XLSX.utils.book_new();
      const wsSummary = XLSX.utils.json_to_sheet(summaryRows);
      const wsDaily   = XLSX.utils.json_to_sheet(dailyRows);
      const wsMeta    = XLSX.utils.json_to_sheet(metaRows);
      // Freeze first row
      wsSummary["!freeze"] = { xSplit: 0, ySplit: 1 };
      wsDaily["!freeze"]   = { xSplit: 0, ySplit: 1 };
      XLSX.utils.book_append_sheet(wb, wsSummary, "Summary");
      XLSX.utils.book_append_sheet(wb, wsDaily,   "Daily");
      XLSX.utils.book_append_sheet(wb, wsMeta,    "Meta");

      const out = XLSX.write(wb, { type: "array", bookType: "xlsx" });
      const blob = new Blob([out], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
      triggerDownload(blob, `ads-${level}-${since}_to_${until}.xlsx`);
    } catch (e) {
      alert("Export ล้มเหลว: " + (e.message || e));
    } finally {
      setExporting(false);
    }
  };

  const ranges = [
    { k: "today",     l: "วันนี้" },
    { k: "yesterday", l: "เมื่อวาน" },
    { k: "7d",  l: "7 วัน" },
    { k: "14d", l: "14 วัน" },
    { k: "30d", l: "30 วัน" },
    { k: "6m",  l: "6 เดือน" },
    { k: "1y",  l: "1 ปี" },
  ];

  return (
    <div className="screen">
      <div className="screen-head">
        <div>
          <div className="breadcrumb">Wandee</div>
          <h1 className="page-title">Campaign Overview</h1>
        </div>
        <div className="head-actions" style={{ flexWrap: "wrap", gap: 6 }}>
          <div className="range-tabs">
            {ranges.map(r => (
              <button key={r.k} className={timeRange === r.k && !customApplied ? "active" : ""}
                onClick={() => { setTimeRange(r.k); clearCustom(); }}>{r.l}</button>
            ))}
            <div style={{ position: "relative" }}>
              <button className={showCustom || customApplied ? "active" : ""}
                onClick={() => setShowCustom(v => !v)}
                style={{ display: "flex", alignItems: "center", gap: 5 }}>
                <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2V6M8 2V6M3 10H21"/></svg>
                {customSince && customUntil
                  ? `${new Date(customSince+'T00:00:00').toLocaleDateString('th-TH',{day:'numeric',month:'short'})} – ${new Date(customUntil+'T00:00:00').toLocaleDateString('th-TH',{day:'numeric',month:'short'})}`
                  : "กำหนดเอง"}
              </button>
              {showCustom && (
                <DateRangePicker since={customSince} until={customUntil}
                  onChange={(s, e) => applyCustom(s, e)}
                  onClose={() => setShowCustom(false)} />
              )}
            </div>
          </div>
          {/* Shop Type multi-select — ใหญ่กว่า filter อื่น */}
          {/*  มีผลเฉพาะ Real Order mode (Meta side ไม่อิง orders) */}
          <div style={{ position: "relative", opacity: dataSource === "meta" ? 0.5 : 1 }}>
            <button
              onClick={() => { if (dataSource === "real") setShopMenuOpen(v => !v); }}
              disabled={dataSource === "meta"}
              title={dataSource === "meta"
                ? "Shop Type filter ใช้ได้เฉพาะ Real Order mode (Meta side ไม่อิง orders)"
                : "กรองตามประเภทช่องทางขาย (เลือกได้หลายอย่าง)"}
              style={{
                height: 32, padding: "0 12px",
                border: `1px solid ${shopTypes.length && dataSource === "real" ? "var(--accent)" : "var(--border)"}`,
                borderRadius: 8,
                background: shopTypes.length && dataSource === "real" ? "var(--accent)" : "var(--bg)",
                color: shopTypes.length && dataSource === "real" ? "#fff" : "var(--ink)",
                fontSize: 12, fontWeight: 600,
                cursor: dataSource === "meta" ? "not-allowed" : "pointer", outline: "none",
                minWidth: 145, display: "flex", alignItems: "center", gap: 6,
                justifyContent: "space-between",
              }}>
              <span>
                🏪 {dataSource === "meta" || shopTypes.length === 0
                      ? "ทุก Shop Type"
                      : shopTypes.length === 1 ? shopTypes[0]
                      : `${shopTypes.length} types`}
              </span>
              <svg width="9" height="9" viewBox="0 0 24 24" fill="currentColor" style={{ opacity: 0.7 }}>
                <path d="M7 10L12 15L17 10Z"/>
              </svg>
            </button>
            {shopMenuOpen && (
              <>
                <div onClick={() => setShopMenuOpen(false)}
                  style={{ position: "fixed", inset: 0, zIndex: 50 }}/>
                <div style={{
                  position: "absolute", top: "calc(100% + 4px)", right: 0, zIndex: 51,
                  minWidth: 200, maxHeight: 320, overflowY: "auto",
                  background: "var(--bg)", border: "1px solid var(--border)", borderRadius: 8,
                  boxShadow: "0 8px 24px rgba(0,0,0,.12)",
                  padding: 4,
                }}>
                  {/* Clear all */}
                  {shopTypes.length > 0 && (
                    <div onClick={() => setShopTypes([])}
                      style={{ padding: "6px 10px", fontSize: 11, color: "var(--danger, #ef4444)", cursor: "pointer", borderBottom: "1px solid var(--border)", marginBottom: 4 }}>
                      ✕ เคลียร์ทั้งหมด
                    </div>
                  )}
                  {shopTypeOptions.length === 0 && (
                    <div style={{ padding: 12, fontSize: 11, color: "var(--ink-faint)", textAlign: "center" }}>
                      ไม่มีข้อมูล shop_type
                    </div>
                  )}
                  {shopTypeOptions.map(st => {
                    const checked = shopTypes.includes(st);
                    return (
                      <label key={st} style={{
                        display: "flex", alignItems: "center", gap: 8, padding: "6px 10px",
                        cursor: "pointer", borderRadius: 4, fontSize: 12,
                        background: checked ? "var(--accent-bg, #eef2ff)" : "transparent",
                      }}
                      onMouseEnter={e => { if (!checked) e.currentTarget.style.background = "var(--bg-elevated, #f5f5f5)"; }}
                      onMouseLeave={e => { if (!checked) e.currentTarget.style.background = "transparent"; }}>
                        <input type="checkbox" checked={checked}
                          onChange={() => toggleShopType(st)}
                          style={{ accentColor: "var(--accent)", width: 14, height: 14, cursor: "pointer" }}/>
                        <span style={{ color: checked ? "var(--accent)" : "var(--ink)", fontWeight: checked ? 600 : 400 }}>{st}</span>
                      </label>
                    );
                  })}
                </div>
              </>
            )}
          </div>
          {/* Data source toggle */}
          <div style={{ display: "flex", alignItems: "center", gap: 0, border: "1px solid var(--border)", borderRadius: 7, overflow: "hidden", height: 28 }}>
            {[
              { k: "meta", l: "Meta", t: "Purchase data จาก Facebook Pixel/CAPI" },
              { k: "real", l: "Real Order", t: "Order data จากระบบจริง (Shop/CRM)" },
            ].map(o => (
              <button key={o.k} onClick={() => setDataSource(o.k)} title={o.t}
                style={{
                  padding: "0 10px", height: 28, border: "none",
                  background: dataSource === o.k ? "var(--accent)" : "transparent",
                  color: dataSource === o.k ? "#fff" : "var(--ink-faint)",
                  fontSize: 11, fontWeight: 600, cursor: "pointer",
                  display: "flex", alignItems: "center", gap: 4,
                }}>
                {o.k === "meta" && (
                  <svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><path d="M20 12.06A8 8 0 1011.18 20v-5.6H9.08V12h2.1V10.18c0-2.07 1.24-3.22 3.13-3.22.9 0 1.85.16 1.85.16v2.03h-1.04c-1.03 0-1.35.64-1.35 1.3V12h2.3l-.37 2.4h-1.93V20A8 8 0 0020 12.06z"/></svg>
                )}
                {o.l}
              </button>
            ))}
          </div>
          <div ref={exportMenuRef} style={{ position: "relative" }}>
            <button
              className="btn-ghost-sm"
              title="Export data"
              disabled={exporting}
              onClick={() => setExportOpen(o => !o)}
            >
              <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 15V19A2 2 0 0119 21H5A2 2 0 013 19V15M7 10L12 15L17 10M12 15V3"/></svg>
              {exporting ? "กำลัง export…" : "Export"}
              <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" style={{ marginLeft: 2 }}><path d="M6 9L12 15L18 9"/></svg>
            </button>
            {exportOpen && (
              <div style={{
                position: "absolute", top: "calc(100% + 4px)", right: 0, zIndex: 50,
                background: "var(--panel)", border: "1px solid var(--border)", borderRadius: 8,
                boxShadow: "0 6px 24px rgba(0,0,0,0.18)", minWidth: 180, padding: 4,
              }}>
                <button
                  onClick={exportJSON}
                  style={{
                    display: "flex", alignItems: "center", gap: 8, width: "100%",
                    padding: "8px 10px", border: "none", background: "transparent",
                    cursor: "pointer", borderRadius: 6, fontSize: 12, color: "var(--ink)",
                    textAlign: "left",
                  }}
                  onMouseEnter={e => e.currentTarget.style.background = "var(--row-hover)"}
                  onMouseLeave={e => e.currentTarget.style.background = "transparent"}
                >
                  <span style={{ fontSize: 14 }}>📄</span>
                  <div style={{ flex: 1 }}>
                    <div style={{ fontWeight: 600 }}>JSON</div>
                    <div style={{ fontSize: 10, color: "var(--ink-faint)" }}>แยกตามแคมเปญ + segment</div>
                  </div>
                </button>
                <button
                  onClick={exportXLSX}
                  style={{
                    display: "flex", alignItems: "center", gap: 8, width: "100%",
                    padding: "8px 10px", border: "none", background: "transparent",
                    cursor: "pointer", borderRadius: 6, fontSize: 12, color: "var(--ink)",
                    textAlign: "left",
                  }}
                  onMouseEnter={e => e.currentTarget.style.background = "var(--row-hover)"}
                  onMouseLeave={e => e.currentTarget.style.background = "transparent"}
                >
                  <span style={{ fontSize: 14 }}>📊</span>
                  <div style={{ flex: 1 }}>
                    <div style={{ fontWeight: 600 }}>Excel (.xlsx)</div>
                    <div style={{ fontSize: 10, color: "var(--ink-faint)" }}>Summary + Daily + Meta</div>
                  </div>
                </button>
              </div>
            )}
          </div>
          <button className="btn-primary" onClick={onOpenBuilder}>
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><path d="M12 4V20M4 12H20"/></svg>
            สร้างแคมเปญ
          </button>
        </div>
      </div>

      {/* Scope hint — บอกว่า KPI กำลังคำนวณจากอะไร */}
      {stats?.scope && stats.scope !== "all" && (() => {
        const levelLabel = stats.scope === "ad" ? "Ad" : stats.scope === "adset" ? "Ad Set" : "Campaign";
        // Extract CBO-XXX prefix code for emphasis (e.g. "CBO-Backpack ..." → "Backpack")
        // Pattern: CBO-(word) followed by whitespace
        let cboCode = null;
        if (stats.entityName) {
          const m = stats.entityName.match(/CBO-(\S+?)\s/);
          if (m) cboCode = m[1];
        }
        return (
          <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 10, fontSize: 12, color: "var(--ink-faint)", flexWrap: "wrap" }}>
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
              <path d="M22 3H2L10 12.46V19L14 21V12.46L22 3Z"/>
            </svg>
            KPI คำนวณจาก <strong style={{ color: "var(--ink)" }}>{stats.count} {levelLabel}</strong> ที่เลือก
            {stats.entityName && (
              <span style={{ display: "inline-flex", alignItems: "center", gap: 6, marginLeft: 4 }}>
                <span style={{ opacity: 0.6 }}>(</span>
                {cboCode && (
                  <strong style={{
                    color: "#b45309",      /* dark yellow / amber-700 */
                    fontSize: 20,
                    fontWeight: 800,
                    letterSpacing: 0.4,
                    lineHeight: 1,
                  }}>
                    {cboCode}
                  </strong>
                )}
                <span style={{ color: "var(--ink)", fontWeight: 500, maxWidth: 480, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                  {stats.entityName}
                </span>
                <span style={{ opacity: 0.6 }}>)</span>
              </span>
            )}
          </div>
        );
      })()}
      {/* Account hint — เตือนเมื่อเลือก account → organic orders ถูกตัดออก */}
      {dataSource === "real" && adAccountId && (
        <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 10, fontSize: 11, color: "var(--ink-faint)" }}>
          <svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor" style={{ opacity: 0.6 }}>
            <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" strokeWidth="2"/>
            <path d="M12 8V13M12 16V16.01" stroke="currentColor" strokeWidth="2"/>
          </svg>
          แสดงเฉพาะ orders ที่มี ad ใน <strong style={{ color: "var(--ink)" }}>{AD_ACCOUNTS.find(a => a.id === adAccountId)?.name || 'Account นี้'}</strong>
          <span style={{ opacity: 0.7 }}>— organic orders ไม่ถูกนับ (เลือก "ทุก Account" เพื่อเห็นรวม)</span>
        </div>
      )}

      {/* KPI Cards — ใช้ live stats */}
      <div className="kpi-grid" style={{ position: "relative", opacity: loading ? 0.5 : 1, transition: "opacity 0.2s", pointerEvents: loading ? "none" : "auto" }}>
        {loading && (
          <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", zIndex: 5 }}>
            <span style={{ display: "flex", alignItems: "center", gap: 8, background: "var(--bg)", padding: "8px 16px", borderRadius: 20, boxShadow: "0 2px 12px rgba(0,0,0,.1)", fontSize: 12, color: "var(--ink-faint)" }}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2.5" style={{ animation: "spin 0.8s linear infinite" }}><path d="M21 12A9 9 0 1112 3"/></svg>
              กำลังโหลด…
            </span>
          </div>
        )}
        {(() => {
          const spendPct = stats && stats.revenue > 0 ? (stats.spend / stats.revenue) * 100 : null;
          const revInclPending = stats ? (stats.revenue + (stats.pendingRevenue || 0)) : 0;
          const spendPctIncl = stats && revInclPending > 0 && (stats.pendingRevenue || 0) > 0
            ? (stats.spend / revInclPending) * 100
            : null;
          const pctColor = spendPct == null ? "var(--ink-faint)"
            : spendPct <= 10 ? "var(--success)"
            : spendPct <= 20 ? "#65a30d"     // lime / olive green
            : spendPct <= 30 ? "var(--warning)"  // amber/yellow
            : spendPct <= 50 ? "#ea580c"     // orange
            : "var(--danger)";              // red
          // Alternate: 0 = confirmed (colored), 1 = incl. pending (gray)
          const showIncl = spendPctIncl != null && spendPctMode === 1;
          const activePct   = showIncl ? spendPctIncl : spendPct;
          const activeColor = showIncl ? "var(--ink-faint)" : pctColor;
          const activeNote  = showIncl ? "spend/รวมรอโอน" : "spend/rev";
          return (
            <KpiCard
              label="ค่าโฆษณารวม (Spend)"
              value={
                <span>
                  {stats ? fmt.money(stats.spend) : "—"}
                  {activePct != null && (
                    <span
                      key={spendPctMode}
                      style={{
                        fontSize: 13, fontWeight: 600, marginLeft: 6, color: activeColor,
                        display: "inline-block",
                        animation: "spendPctFade 5s ease-in-out",
                      }}
                    >
                      ({activePct.toFixed(1)}%)
                    </span>
                  )}
                </span>
              }
              sublabel={stats ? `${stats.count} ${stats.scope === "ad" ? "Ad" : stats.scope === "adset" ? "Ad Set" : "Campaign"}${activePct != null ? ` · ${activeNote}` : ""}` : ""}
              color="var(--accent)"
            />
          );
        })()}
        <KpiCard
          label="CTR เฉลี่ย"
          value={stats ? fmt.pct(stats.ctr) : "—"}
          sublabel={stats ? `${fmt.num(stats.clicks)} clicks / ${fmt.num(stats.impressions)} impr` : ""}
          color="var(--success)"
        />
        <KpiCard
          label="ROAS เฉลี่ย"
          value={stats ? (stats.spend > 0 ? stats.roas.toFixed(2) + "×" : "—") : "—"}
          sublabel={stats
            ? (stats.revenue === 0 && stats.spend > 0
                ? "ยังไม่มี order data"
                : `${fmt.money(stats.revenue)} / ${fmt.money(stats.spend)}`)
            : ""}
          color={stats && stats.roas >= 2 ? "var(--success)" : stats && stats.roas >= 1 ? "var(--warning)" : "var(--danger)"}
        />
        <KpiCard
          label="กำไร (Gross)"
          value={stats ? fmt.money(stats.revenue - stats.spend) : "—"}
          sublabel={stats && stats.revenue > 0 ? `margin ${(((stats.revenue - stats.spend) / stats.revenue) * 100).toFixed(1)}%` : "ยังไม่มีรายได้"}
          color="var(--accent)"
        />
        <KpiCard
          label="ต้องปรับปรุง"
          value={stats ? stats.needsAttention : "—"}
          sublabel="ROAS < 1.5×"
          icon={<svg width="14" height="14" viewBox="0 0 24 24" fill="var(--warning)"><path d="M12 2L22 20H2Z M12 9V14 M12 17V18.5" stroke="white" strokeWidth="1.5"/></svg>}
        />
      </div>

      {/* Cost breakdown row — ใช้ live stats */}
      <div className="cost-row">
        <div className="cost-cell revenue-feature">
          <span>รายได้รวม <span style={{ color: "var(--success)", fontSize: 9 }}>✅ โอนแล้ว</span></span>
          <strong>{stats ? fmt.money(stats.revenue) : "—"}</strong>
          {dataSource === "real" && stats && stats.pendingOrders > 0 && (
            <span style={{ fontSize: 10, color: "var(--warning)", marginTop: 2 }}>
              ⏳ รอโอน {fmt.money(stats.pendingRevenue)} ({stats.pendingOrders})
            </span>
          )}
          {dataSource === "real" && stats && stats.pendingRevenue > 0 && (
            <span style={{ fontSize: 11, color: "var(--ink-soft, var(--ink))", fontWeight: 600, marginTop: 2, opacity: 0.85 }}>
              = {fmt.money(stats.revenue + stats.pendingRevenue)} <span style={{ fontWeight: 400, opacity: 0.7 }}>(รวม)</span>
            </span>
          )}
        </div>
        <span className="cost-op">−</span>
        <div className="cost-cell">
          <span>ค่าโฆษณา</span>
          <strong>{stats ? fmt.money(stats.spend) : "—"}</strong>
        </div>
        <span className="cost-op">=</span>
        <div className="cost-cell highlight">
          <span>กำไรขั้นต้น</span>
          <strong>{stats ? fmt.money(stats.revenue - stats.spend) : "—"}</strong>
          {dataSource === "real" && stats && stats.pendingRevenue > 0 && (
            <span style={{ fontSize: 10, color: "var(--ink-faint)", marginTop: 2 }}>
              max {fmt.money((stats.revenue + stats.pendingRevenue) - stats.spend)}
            </span>
          )}
        </div>
        <div className="cost-cell compact">
          <span>Orders</span>
          <strong>{stats ? fmt.num(stats.orders) : "—"}</strong>
          {dataSource === "real" && stats && stats.pendingOrders > 0 && (
            <span style={{ fontSize: 10, color: "var(--warning)", marginTop: 2 }}>
              ⏳ +{fmt.num(stats.pendingOrders)} รอโอน
            </span>
          )}
          {dataSource === "real" && stats && stats.cancelledOrders > 0 && (
            <span style={{ fontSize: 10, color: "var(--danger)", marginTop: 2 }}>
              ❌ +{fmt.num(stats.cancelledOrders)} ยกเลิก
            </span>
          )}
        </div>
        <div className="cost-cell compact">
          <span>AOV</span>
          <strong>{stats && stats.orders > 0 ? fmt.money(stats.aov) : "—"}</strong>
        </div>
        <div className="cost-cell compact">
          <span>CPA</span>
          <strong>{stats && stats.orders > 0 ? fmt.money(stats.spend / stats.orders) : "—"}</strong>
        </div>
        <div className="cost-cell compact">
          <span>CPC</span>
          <strong>{stats && stats.clicks > 0 ? "฿" + stats.cpc.toFixed(1) : "—"}</strong>
        </div>
      </div>

      {/* Customer Breakdown — แสดงเฉพาะ Real Order mode */}
      {dataSource === "real" && (
        <CustomerBreakdown
          timeRange={timeRange}
          customSince={customSince}
          customUntil={customUntil}
          customApplied={customApplied}
          adAccountId={adAccountId}
          scope={stats?.scope || "all"}
          scopeIds={stats?.scopeIds || null}
          shopTypes={shopTypes}
        />
      )}

      {/* Embedded Campaigns/Ad Sets/Ads hierarchy (FB-style tabs) */}
      <CampaignsHierarchy
        timeRange={timeRange}
        setTimeRange={setTimeRange}
        adAccountId={adAccountId}
        hideHeader={true}
        customSince={customSince}
        customUntil={customUntil}
        customApplied={customApplied}
        onApplyCustom={applyCustom}
        onClearCustom={clearCustom}
        onSelectCampaign={onSelectCampaign}
        onSelectAd={onSelectAd}
        onStatsChange={setStats}
        onLoadingChange={setLoading}
        dataSource={dataSource}
        shopFilter={shopFilter}
      />
    </div>
  );
};

/* ================ CUSTOMER BREAKDOWN (Real Order only) ================ */
const CustomerBreakdown = ({ timeRange, customSince, customUntil, customApplied, adAccountId, scope = "all", scopeIds = null, shopTypes = [] }) => {
  const [data, setData]       = React.useState(null);
  const [cohort, setCohort]   = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  // Switch: 'orders' หรือ 'customers' (period-relative segment)
  const [viewMode, setViewMode] = React.useState(() => Urlp.get("cbMode") === "customers" ? "customers" : "orders");
  React.useEffect(() => { Urlp.patch({ cbMode: viewMode === "orders" ? null : "customers" }); }, [viewMode]);

  // resolve date range — sync กับ Dashboard preset logic
  const { since, until } = React.useMemo(() => {
    if (customApplied && customSince && customUntil) return { since: customSince, until: customUntil };
    const today = new Date(); today.setHours(0,0,0,0);
    const ymd = (d) => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`;
    // ⭐ Single-day presets — since=until=วันเดียว
    if (timeRange === "today") return { since: ymd(today), until: ymd(today) };
    if (timeRange === "yesterday") {
      const y = new Date(today); y.setDate(y.getDate() - 1);
      return { since: ymd(y), until: ymd(y) };
    }
    // Range presets — N วันที่ผ่านมา ไม่รวมวันนี้ (FB convention, ตรงกับ DateRangePicker)
    const map = { "7d": 7, "14d": 14, "30d": 30, "6m": 180, "1y": 365 };
    const n = map[timeRange] ?? 7;
    const s = new Date(today); s.setDate(s.getDate() - n);
    return { since: ymd(s), until: ymd(today) };
  }, [timeRange, customApplied, customSince, customUntil]);

  // serialize scope IDs for deps comparison
  const scopeIdsKey = scopeIds ? scopeIds.join(",") : "";

  const shopTypesKey = (shopTypes || []).join(",");
  React.useEffect(() => {
    let cancelled = false;
    setLoading(true);
    const tasks = [
      window.DB?.getCustomerBreakdown ? window.DB.getCustomerBreakdown({ since, until, adAccountId, mode: viewMode, scope, scopeIds, shopTypes }) : Promise.resolve(null),
      window.DB?.getAcquisitionCohort ? window.DB.getAcquisitionCohort({ since, until }) : Promise.resolve([]),
    ];
    Promise.all(tasks)
      .then(([res, coh]) => { if (!cancelled) { setData(res); setCohort(coh || []); setLoading(false); } })
      .catch(err => { console.error("[CustomerBreakdown]", err); if (!cancelled) { setData(null); setCohort([]); setLoading(false); } });
    return () => { cancelled = true; };
  }, [since, until, adAccountId, viewMode, scope, scopeIdsKey, shopTypesKey]);

  const segDefs = [
    { k: "new",       l: "ลูกค้าใหม่",        icon: "🆕", color: "#3b82f6" },
    { k: "loyal",     l: "ลูกค้าประจำ",       icon: "💚", color: "#10b981" },
    { k: "returning", l: "ลูกค้าเก่า",        icon: "🟡", color: "#f59e0b" },
    { k: "dormant",   l: "ลูกค้าหลับ (1y+)",  icon: "🔴", color: "#ef4444" },
  ];
  const stDefs = [
    { k: "pending",   l: "รอโอน" },
    { k: "confirmed", l: "โอนแล้ว" },
    { k: "cancelled", l: "ยกเลิก" },
  ];

  // Switch UI
  const ViewSwitch = (
    <div style={{ display: "inline-flex", border: "1px solid var(--border)", borderRadius: 6, overflow: "hidden", height: 22 }}>
      {[
        { k: "orders",    l: "📋 Orders",    t: "นับแต่ละ order (segment snapshot ตอนสั่ง)" },
        { k: "customers", l: "👥 Customers", t: "นับ unique ลูกค้า (segment period-relative)" },
      ].map(o => (
        <button key={o.k} onClick={() => setViewMode(o.k)} title={o.t}
          style={{
            padding: "0 8px", border: "none", height: 22, cursor: "pointer", fontSize: 10, fontWeight: 600,
            background: viewMode === o.k ? "var(--accent)" : "transparent",
            color: viewMode === o.k ? "#fff" : "var(--ink-faint)",
          }}>{o.l}</button>
      ))}
    </div>
  );

  if (loading) return (
    <div style={{ padding: 20, textAlign: "center", color: "var(--ink-faint)", fontSize: 12 }}>
      กำลังโหลด customer breakdown…
    </div>
  );
  const noData = !data || data.totals.count === 0;
  if (noData) return (
    <div style={{ padding: 16, textAlign: "center", color: "var(--ink-faint)", fontSize: 12, border: "1px dashed var(--border)", borderRadius: 8, margin: "12px 0" }}>
      ยังไม่มี order data ในช่วงนี้
    </div>
  );

  const { matrix, source, totals } = data;
  const cellOf = (seg, st) => matrix[seg][st] || { count: 0, revenue: 0 };
  const rowTotal = (seg) => stDefs.reduce((sum, st) => sum + cellOf(seg, st.k).count, 0);
  const colTotal = (st)  => segDefs.reduce((sum, seg) => sum + cellOf(seg.k, st).count, 0);
  const unitLabel = viewMode === "customers" ? "คน" : "ออเดอร์";

  return (
    <div style={{ margin: "12px 0 16px" }}>
      <div style={{ display: "grid", gridTemplateColumns: "1.5fr 1fr", gap: 12 }}>
        {/* Segment × Status Matrix */}
        <div style={{ border: "1px solid var(--border)", borderRadius: 8, padding: 12, background: "var(--bg)" }}>
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8, gap: 8, flexWrap: "wrap" }}>
            <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
              <strong style={{ fontSize: 12 }}>Customer Segment × Status</strong>
              {ViewSwitch}
            </div>
            <span style={{ fontSize: 10, color: "var(--ink-faint)" }}>{since} → {until}</span>
          </div>
          <table style={{ width: "100%", fontSize: 11, borderCollapse: "collapse" }}>
            <thead>
              <tr style={{ color: "var(--ink-faint)" }}>
                <th style={{ textAlign: "left", padding: "4px 6px", fontWeight: 500 }}>Segment</th>
                {stDefs.map(st => <th key={st.k} style={{ textAlign: "right", padding: "4px 6px", fontWeight: 500 }}>{st.l}</th>)}
                <th style={{ textAlign: "right", padding: "4px 6px", fontWeight: 600 }}>รวม</th>
              </tr>
            </thead>
            <tbody>
              {segDefs.map(seg => (
                <tr key={seg.k} style={{ borderTop: "1px solid var(--border)" }}>
                  <td style={{ padding: "5px 6px" }}>
                    <span style={{ marginRight: 4 }}>{seg.icon}</span>{seg.l}
                  </td>
                  {stDefs.map(st => {
                    const cell = cellOf(seg.k, st.k);
                    return (
                      <td key={st.k} style={{ textAlign: "right", padding: "5px 6px", color: cell.count === 0 ? "var(--ink-faint)" : "var(--ink)" }}>
                        {cell.count}
                        {cell.revenue > 0 && (
                          <div style={{ fontSize: 9, color: "var(--ink-faint)" }}>฿{fmt.num(cell.revenue)}</div>
                        )}
                      </td>
                    );
                  })}
                  <td style={{ textAlign: "right", padding: "5px 6px", fontWeight: 600 }}>{rowTotal(seg.k)}</td>
                </tr>
              ))}
              <tr style={{ borderTop: "2px solid var(--border)", fontWeight: 600 }}>
                <td style={{ padding: "5px 6px" }}>รวม</td>
                {stDefs.map(st => <td key={st.k} style={{ textAlign: "right", padding: "5px 6px" }}>{colTotal(st.k)}</td>)}
                <td style={{ textAlign: "right", padding: "5px 6px" }}>{totals.count}</td>
              </tr>
            </tbody>
          </table>
          <div style={{ display: "flex", gap: 12, marginTop: 8, paddingTop: 8, borderTop: "1px solid var(--border)", fontSize: 10, color: "var(--ink-faint)", flexWrap: "wrap" }}>
            <span>📋 Orders: <strong style={{ color: "var(--ink)" }}>{fmt.num(totals.orders || totals.count)}</strong></span>
            <span>👥 Unique customers: <strong style={{ color: "var(--ink)" }}>{fmt.num(totals.customers)}</strong></span>
            <span>💰 AOV: <strong style={{ color: "var(--ink)" }}>฿{fmt.num(Math.round(totals.aov || 0))}</strong></span>
            <span>❌ Cancel rate: <strong style={{ color: totals.cancelRate > 10 ? "var(--danger)" : "var(--ink)" }}>{(totals.cancelRate || 0).toFixed(1)}%</strong></span>
            <span style={{ marginLeft: "auto", fontStyle: "italic" }}>หน่วย: {unitLabel}</span>
          </div>
        </div>

        {/* Source Split */}
        <div style={{ border: "1px solid var(--border)", borderRadius: 8, padding: 12, background: "var(--bg)" }}>
          <strong style={{ fontSize: 12, display: "block", marginBottom: 8 }}>Source Split</strong>
          {[
            { k: "organic",    l: "🔵 Organic",    t: "ไม่ระบุ ad (รวม pending ที่ยังไม่ tag)" },
            { k: "attributed", l: "🟢 Attributed", t: "มาจาก ad" },
            { k: "cancelled",  l: "❌ Cancelled",   t: "ยกเลิก" },
          ].map(s => {
            const cell = source[s.k];
            const pct = totals.count > 0 ? (cell.count / totals.count) * 100 : 0;
            return (
              <div key={s.k} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "6px 0", borderBottom: "1px solid var(--border)" }}>
                <div>
                  <div style={{ fontSize: 11 }}>{s.l}</div>
                  <div style={{ fontSize: 9, color: "var(--ink-faint)" }}>{s.t}</div>
                </div>
                <div style={{ textAlign: "right" }}>
                  <div style={{ fontSize: 12, fontWeight: 600 }}>{cell.count} <span style={{ fontSize: 9, color: "var(--ink-faint)", fontWeight: 400 }}>({pct.toFixed(0)}%)</span></div>
                  <div style={{ fontSize: 10, color: "var(--ink-faint)" }}>฿{fmt.num(cell.revenue)}</div>
                </div>
              </div>
            );
          })}
          <div style={{ marginTop: 8, paddingTop: 6, fontSize: 10, color: "var(--ink-faint)" }}>
            Attribution rate: <strong style={{ color: "var(--ink)" }}>
              {totals.count > 0 ? ((source.attributed.count / totals.count) * 100).toFixed(1) : 0}%
            </strong>
          </div>
        </div>
      </div>

      {/* Acquisition Cohort — แสดงเฉพาะถ้ามี new customer และ period > 1 เดือน หรือมี > 1 month */}
      {cohort && cohort.length > 0 && (
        <div style={{ marginTop: 12, border: "1px solid var(--border)", borderRadius: 8, padding: 12, background: "var(--bg)" }}>
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 8 }}>
            <strong style={{ fontSize: 12 }}>🆕 Acquisition Cohort</strong>
            <span style={{ fontSize: 10, color: "var(--ink-faint)" }}>
              new customers acquired per month · {cohort.reduce((s, c) => s + c.count, 0)} ใน period
            </span>
          </div>
          <AcquisitionCohortChart data={cohort} />
        </div>
      )}
    </div>
  );
};

/* ================ ACQUISITION COHORT CHART ================ */
const AcquisitionCohortChart = ({ data }) => {
  if (!data || !data.length) return null;
  const max = Math.max(...data.map(d => d.count), 1);
  const MONTHS_TH = ["ม.ค.","ก.พ.","มี.ค.","เม.ย.","พ.ค.","มิ.ย.","ก.ค.","ส.ค.","ก.ย.","ต.ค.","พ.ย.","ธ.ค."];
  const formatMonth = (ym) => {
    const [y, m] = ym.split("-");
    return `${MONTHS_TH[parseInt(m,10)-1]} ${parseInt(y,10)+543-2500}`;  // พ.ศ. แบบสั้น
  };

  return (
    <div>
      <div style={{ display: "flex", alignItems: "flex-end", gap: 4, height: 80, paddingTop: 12 }}>
        {data.map(d => {
          const h = (d.count / max) * 100;
          return (
            <div key={d.month} title={`${formatMonth(d.month)}: ${d.count} new customers · ฿${fmt.num(d.revenue || 0)} LTV รวม`}
              style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", minWidth: 30, position: "relative" }}>
              <div style={{ fontSize: 10, color: "var(--ink)", marginBottom: 2, fontWeight: 600 }}>{d.count}</div>
              <div style={{ width: "100%", height: `${h}%`, minHeight: 2, background: "linear-gradient(to top, var(--accent), var(--accent) 60%, var(--accent-light, var(--accent)))", borderRadius: "3px 3px 0 0" }} />
            </div>
          );
        })}
      </div>
      <div style={{ display: "flex", gap: 4, marginTop: 4 }}>
        {data.map(d => (
          <div key={d.month} style={{ flex: 1, fontSize: 9, color: "var(--ink-faint)", textAlign: "center", minWidth: 30 }}>
            {formatMonth(d.month)}
          </div>
        ))}
      </div>
    </div>
  );
};

/* ================ CAMPAIGNS HIERARCHY ================ */
const CampaignsHierarchy = ({
  timeRange, setTimeRange, adAccountId, onSelectCampaign, onSelectAd, hideHeader = false,
  // controlled date range (when embedded inside Dashboard)
  customSince:   propSince,
  customUntil:   propUntil,
  customApplied: propApplied,
  onApplyCustom, onClearCustom,
  // KPI callback — push aggregated stats กลับขึ้นไปให้ Dashboard
  onStatsChange,
  onLoadingChange,
  // "meta" (FB pixel/CAPI) | "real" (จากตาราง orders เรา)
  dataSource = "meta",
  // กรอง campaign/adset/ad → ตาม shop_type filter (null = ไม่กรอง)
  // shape: { adIds, adsetIds, campaignIds } (sets of allowed UUIDs)
  shopFilter = null,
}) => {
  const PAGE_SIZE = 50;

  // local fallback (used only in standalone mode = hideHeader=false)
  const [localSince,   setLocalSince]   = React.useState("");
  const [localUntil,   setLocalUntil]   = React.useState("");
  const [localApplied, setLocalApplied] = React.useState(false);
  const [showCustom,   setShowCustom]   = React.useState(false);

  // controlled props win
  const customSince   = propSince   !== undefined ? propSince   : localSince;
  const customUntil   = propUntil   !== undefined ? propUntil   : localUntil;
  const customApplied = propApplied !== undefined ? propApplied : localApplied;

  const applyCustom = (s, e) => {
    if (onApplyCustom) onApplyCustom(s, e);
    else { setLocalSince(s); setLocalUntil(e); setLocalApplied(!!(s && e)); }
    setShowCustom(false);
  };
  const clearCustom = () => {
    if (onClearCustom) onClearCustom();
    else { setLocalSince(""); setLocalUntil(""); setLocalApplied(false); }
    setShowCustom(false);
  };

  // local data states (re-fetched when date range/filter changes)
  // ⭐ เริ่มว่าง ถ้ามี DB (กัน mock data flash ตอน reload); ใช้ mock เฉพาะ demo mode (ไม่มี DB)
  const [campaigns, setCampaigns] = React.useState(() => window.DB ? [] : (window.MOCK.campaigns || []));
  const [adSets,    setAdSets]    = React.useState([]);
  const [ads,       setAds]       = React.useState([]);
  const [fetching,  setFetching]  = React.useState(false);

  const DAYS_MAP = { "7d": 7, "14d": 14, "30d": 30, "6m": 180, "1y": 365 };
  // ช่วงพิเศษ (วันเดียว) ส่งเป็น { since, until } ตรงๆ
  // ใช้ local date เพื่อกัน timezone bug (toISOString จะให้ UTC ซึ่งคลาดเคลื่อน +7)
  const ymd = (d) => {
    const y = d.getFullYear();
    const m = String(d.getMonth() + 1).padStart(2, "0");
    const day = String(d.getDate()).padStart(2, "0");
    return `${y}-${m}-${day}`;
  };
  const PRESET_RANGE = (key) => {
    const now = new Date(); now.setHours(0,0,0,0);
    if (key === "today") return { since: ymd(now), until: ymd(now) };
    if (key === "yesterday") {
      const y = new Date(now); y.setDate(y.getDate() - 1);
      return { since: ymd(y), until: ymd(y) };
    }
    return null;
  };

  // FB Ads Manager–style level tabs — sync จาก URL
  const [level, setLevel] = React.useState(() => {
    const t = Urlp.get("tab");
    return ["campaign", "adset", "ad"].includes(t) ? t : "campaign";
  });

  // ✅ แยก state เป็น 2 ก้อน — sync จาก URL
  const [selectedByLevel, setSelectedByLevel] = React.useState(() => ({
    campaign: Urlp.setFromParam("selC"),
    adset:    Urlp.setFromParam("selA"),
    ad:       Urlp.setFromParam("selD"),
  }));
  const [filterByLevel, setFilterByLevel] = React.useState(() => ({
    adset: Urlp.setFromParam("fltA"),
    ad:    Urlp.setFromParam("fltD"),
  }));

  // anchor สำหรับ shift+click range-select ของแต่ละ level
  const [lastClickedByLevel, setLastClickedByLevel] = React.useState({
    campaign: null, adset: null, ad: null,
  });

  // ── Resizable name column ───────────────────────────────
  const [nameColW, setNameColW] = React.useState(() => {
    const saved = parseInt(localStorage.getItem("ads.nameColW") || "", 10);
    return Number.isFinite(saved) && saved >= 140 ? saved : 320;
  });
  const resizerRef = React.useRef(null);
  React.useEffect(() => { localStorage.setItem("ads.nameColW", String(nameColW)); }, [nameColW]);

  const onResizeStart = (e) => {
    e.preventDefault(); e.stopPropagation();
    const startX = e.clientX;
    const startW = nameColW;
    document.body.style.cursor = "col-resize";
    resizerRef.current?.classList.add("resizing");
    const onMove = (ev) => {
      const w = Math.max(140, Math.min(600, startW + (ev.clientX - startX)));
      setNameColW(w);
    };
    const onUp = () => {
      document.body.style.cursor = "";
      resizerRef.current?.classList.remove("resizing");
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseup", onUp);
    };
    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseup", onUp);
  };

  // derived: ids ที่ใช้กรอง (จาก filterByLevel ของ tab นั้นๆ)
  const campaignFilterIds = React.useMemo(() => [...filterByLevel.adset], [filterByLevel.adset]);
  const adsetFilterIds    = React.useMemo(() => [...filterByLevel.ad],    [filterByLevel.ad]);

  const dateOpts = () => {
    if (customApplied && customSince && customUntil) {
      return { since: customSince, until: customUntil };
    }
    const preset = PRESET_RANGE(timeRange);
    if (preset) return preset;
    return { days: DAYS_MAP[timeRange] ?? 7 };
  };

  // Fetch campaigns
  React.useEffect(() => {
    if (!window.DB) { setCampaigns(window.MOCK.campaigns || []); return; }
    setFetching(true);
    window.DB.getCampaigns(adAccountId || null, dateOpts())
      // ⭐ ถ้า query สำเร็จ ให้ trust ผลลัพธ์ (รวม empty array) — ไม่ fallback ไป MOCK
      // fallback ไป MOCK เฉพาะกรณี error เท่านั้น
      .then(data => setCampaigns(Array.isArray(data) ? data : []))
      .catch(() => setCampaigns([]))   // error ใน live mode → ว่าง (ไม่โชว์ mock ปลอม)
      .finally(() => setFetching(false));
  }, [timeRange, adAccountId, customApplied, customSince, customUntil]);

  // Fetch ad sets (เมื่อเข้า tab adset/ad หรือ campaign filter เปลี่ยน)
  React.useEffect(() => {
    if (level !== "adset" && level !== "ad") return;
    if (!window.DB) { setAdSets(window.MOCK.adSets || []); return; }
    setFetching(true);
    const filter = campaignFilterIds.length ? campaignFilterIds : null;
    window.DB.getAdSets(filter, { ...dateOpts(), adAccountId })
      .then(data => setAdSets(data ?? []))
      .catch(() => setAdSets([]))
      .finally(() => setFetching(false));
  }, [level, campaignFilterIds.join(","), adAccountId, timeRange, customApplied, customSince, customUntil]);

  // Fetch ads (เมื่อเข้า tab ad หรือ adset filter เปลี่ยน)
  React.useEffect(() => {
    if (level !== "ad") return;
    if (!window.DB) { setAds(window.MOCK.ads || []); return; }
    setFetching(true);
    let filter = null;
    if (adsetFilterIds.length) {
      filter = adsetFilterIds;
    } else if (campaignFilterIds.length && adSets.length) {
      filter = adSets.filter(s => campaignFilterIds.includes(s.campaignId)).map(s => s.id);
      if (filter.length === 0) { setAds([]); setFetching(false); return; }
    }
    window.DB.getAds(filter, { ...dateOpts(), adAccountId })
      .then(data => setAds(data ?? []))
      .catch(() => setAds([]))
      .finally(() => setFetching(false));
  }, [level, adsetFilterIds.join(","), campaignFilterIds.join(","), adSets.length, adAccountId, timeRange, customApplied, customSince, customUntil]);

  // table state — sync จาก URL
  const [search, setSearch] = React.useState(() => Urlp.get("q") || "");
  const [statusFilterByLevel, setStatusFilterByLevel] = React.useState(() => ({
    campaign: Urlp.get("stC") || "ALL",
    adset:    Urlp.get("stA") || "ALL",
    ad:       Urlp.get("stD") || "ALL",
  }));
  const statusFilter = statusFilterByLevel[level];
  const setStatusFilter = (v) => setStatusFilterByLevel(p => ({ ...p, [level]: v }));
  const [sortByLevel, setSortByLevel] = React.useState(() => ({
    campaign: Urlp.sortFromParam("soC"),
    adset:    Urlp.sortFromParam("soA"),
    ad:       Urlp.sortFromParam("soD"),
  }));

  // ── Sync state → URL (realtime) ──────────────────────────
  React.useEffect(() => { Urlp.patch({ tab: level === "campaign" ? null : level }); }, [level]);
  React.useEffect(() => { Urlp.patch({ q: search || null }); }, [search]);
  React.useEffect(() => {
    Urlp.patch({
      selC: Urlp.setToParam(selectedByLevel.campaign),
      selA: Urlp.setToParam(selectedByLevel.adset),
      selD: Urlp.setToParam(selectedByLevel.ad),
    });
  }, [selectedByLevel]);
  React.useEffect(() => {
    Urlp.patch({
      fltA: Urlp.setToParam(filterByLevel.adset),
      fltD: Urlp.setToParam(filterByLevel.ad),
    });
  }, [filterByLevel]);
  React.useEffect(() => {
    Urlp.patch({
      stC: statusFilterByLevel.campaign === "ALL" ? null : statusFilterByLevel.campaign,
      stA: statusFilterByLevel.adset    === "ALL" ? null : statusFilterByLevel.adset,
      stD: statusFilterByLevel.ad       === "ALL" ? null : statusFilterByLevel.ad,
    });
  }, [statusFilterByLevel]);
  React.useEffect(() => {
    Urlp.patch({
      soC: Urlp.sortToParam(sortByLevel.campaign),
      soA: Urlp.sortToParam(sortByLevel.adset),
      soD: Urlp.sortToParam(sortByLevel.ad),
    });
  }, [sortByLevel]);
  const sortKey = sortByLevel[level].key;
  const sortDir = sortByLevel[level].dir;
  const setSortKey = (key) => setSortByLevel(p => ({ ...p, [level]: { ...p[level], key } }));
  const setSortDir = (dir) => setSortByLevel(p => ({ ...p, [level]: { ...p[level], dir } }));
  const [page, setPage]                 = React.useState(1);
  // selected = view ของ selection ที่ level ปัจจุบัน (จาก selectedByLevel)
  const selected = selectedByLevel[level];
  const setSelected = (updater) => setSelectedByLevel(prev => ({
    ...prev,
    [level]: typeof updater === "function" ? updater(prev[level]) : updater,
  }));

  // reset page only — selection ของแต่ละ level เก็บค้างไว้
  React.useEffect(() => { setPage(1); }, [search, statusFilter, level, campaignFilterIds.join(","), adsetFilterIds.join(",")]);

  // เมื่อ status filter เปลี่ยน → prune selection/filter ของ id ที่ไม่อยู่ใน "visible" อีกแล้ว
  // เพื่อให้ bulk action กับ filter ตรงกับ tab ที่เห็นจริง
  // ⭐ Guard: skip while initial fetch hasn't populated campaigns yet, otherwise
  //    URL-restored selections (?selC=..., ?fltA=...) get wiped on mount.
  const dataReadyForPruneRef = React.useRef(false);
  React.useEffect(() => {
    // Mark ready once we have campaigns loaded (or load attempt completed with empty result).
    if (!dataReadyForPruneRef.current) {
      if (campaigns.length > 0) dataReadyForPruneRef.current = true;
    }
  }, [campaigns]);
  React.useEffect(() => {
    if (!dataReadyForPruneRef.current) return;
    setSelectedByLevel(prev => {
      const next = { ...prev };
      const pruneSet = (s, allowedSet) => {
        const out = new Set();
        s.forEach(id => { if (allowedSet.has(id)) out.add(id); });
        return out;
      };
      next.campaign = pruneSet(prev.campaign, visibleCampaignIds);
      next.adset    = pruneSet(prev.adset,    visibleAdSetIds);
      next.ad       = pruneSet(prev.ad,       visibleAdIds);
      return next;
    });
    setFilterByLevel(prev => {
      const next = { ...prev };
      const pruneSet = (s, allowedSet) => {
        const out = new Set();
        s.forEach(id => { if (allowedSet.has(id)) out.add(id); });
        return out;
      };
      next.adset = pruneSet(prev.adset, visibleCampaignIds);
      next.ad    = pruneSet(prev.ad,    visibleAdSetIds);
      return next;
    });
  }, [visibleCampaignIds, visibleAdSetIds, visibleAdIds]);

  const ranges = [
    { k: "7d", l: "7 วัน" }, { k: "14d", l: "14 วัน" },
    { k: "30d", l: "30 วัน" }, { k: "6m", l: "6 เดือน" }, { k: "1y", l: "1 ปี" },
  ];

  // คลิกหัวคอลัมน์: ไม่มี sort → desc → asc → กลับ default (null)
  const onSort = (key) => {
    if (sortKey !== key) { setSortKey(key); setSortDir("desc"); }
    else if (sortDir === "desc") setSortDir("asc");
    else { setSortKey(null); setSortDir("desc"); }
  };
  const clearSort = () => { setSortKey(null); setSortDir("desc"); };
  const SortIcon = ({ k }) => sortKey !== k ? (
    // hint icon เล็กๆ จางๆ (เห็นเฉพาะตอน hover)
    <svg width="9" height="9" viewBox="0 0 24 24" fill="currentColor" className="sort-hint"
      style={{ marginLeft: 3, opacity: 0.25 }}><path d="M12 5V19M5 12L12 5L19 12M5 12L12 19L19 12" stroke="currentColor" fill="none" strokeWidth="2"/></svg>
  ) : (
    <svg width="9" height="9" viewBox="0 0 24 24" fill="currentColor" style={{ marginLeft: 3, transform: sortDir === "asc" ? "rotate(180deg)" : "" }}><path d="M12 16L4 8L20 8Z"/></svg>
  );
  const money = (n) => (!n || n === 0) ? <span className="muted">—</span> : fmt.money(n);
  const num   = (n) => (!n || n === 0) ? <span className="muted">—</span> : fmt.num(n);
  const pct   = (n) => (!n || n === 0) ? <span className="muted">—</span> : fmt.pct(n);
  const Th = ({ k, label }) => (
    <div className="h-cell num sortable" onClick={() => onSort(k)} style={{ cursor: "pointer", userSelect: "none" }}>
      {label}<SortIcon k={k} />
    </div>
  );

  // FB effective_status: ACTIVE, PAUSED, CAMPAIGN_PAUSED, ADSET_PAUSED, ARCHIVED ...
  const matchesStatus = (rawStatus, filter) => {
    if (filter === "ALL") return true;
    const s = (rawStatus || "").toUpperCase();
    if (filter === "ACTIVE") return s === "ACTIVE";
    if (filter === "PAUSED") return s !== "ACTIVE" && s !== "";
    return s === filter;
  };

  // ── Cascade status filter: parent → child ──────────────
  // เช่น Campaigns = Active → Ad Sets และ Ads แสดงเฉพาะของ Active campaign
  const visibleCampaignIds = React.useMemo(() => new Set(
    campaigns
      .filter(c => matchesStatus(c.effectiveStatus || c.status, statusFilterByLevel.campaign))
      .map(c => c.id)
  ), [campaigns, statusFilterByLevel.campaign]);

  const visibleAdSetIds = React.useMemo(() => new Set(
    adSets
      .filter(s => visibleCampaignIds.has(s.campaignId))
      .filter(s => matchesStatus(s.effectiveStatus || s.status, statusFilterByLevel.adset))
      .map(s => s.id)
  ), [adSets, visibleCampaignIds, statusFilterByLevel.adset]);

  const visibleAdIds = React.useMemo(() => new Set(
    ads
      .filter(a => visibleAdSetIds.has(a.adSetId))
      .filter(a => matchesStatus(a.effectiveStatus || a.status, statusFilterByLevel.ad))
      .map(a => a.id)
  ), [ads, visibleAdSetIds, statusFilterByLevel.ad]);

  // ── Effective scope สำหรับ KPI aggregation ──────────────
  //  - ถ้ามี selection ที่ level ใด → ใช้ rows ของ level นั้น
  //  - ถ้าไม่มี selection ที่ level ใด → ใช้ "ทั้ง level" (campaign) หลัง status cascade
  const effectiveScope = React.useMemo(() => {
    // ⭐ Apply shopFilter ที่ default scope (เมื่อไม่มี user selection)
    //    ถ้า user เลือก campaigns/adsets/ads เอง → respect selection (intersect shopFilter ด้วย)
    let base;
    if (selectedByLevel.ad.size) {
      base = ads.filter(a => selectedByLevel.ad.has(a.id));
    } else if (selectedByLevel.adset.size) {
      base = adSets.filter(s => selectedByLevel.adset.has(s.id));
    } else if (selectedByLevel.campaign.size) {
      base = campaigns.filter(c => selectedByLevel.campaign.has(c.id));
    } else {
      base = campaigns.filter(c => visibleCampaignIds.has(c.id));
    }
    // intersect ด้วย shopFilter ถ้ามี — ⭐ เฉพาะ Real Order mode (Meta side ไม่อิง orders)
    if (shopFilter && dataSource === "real") {
      const lvl = selectedByLevel.ad.size ? "ad"
                : selectedByLevel.adset.size ? "adset"
                : selectedByLevel.campaign.size ? "campaign"
                : "campaign";   // default scope = campaign level
      const allowedSet = lvl === "ad" ? new Set(shopFilter.adIds || [])
                       : lvl === "adset" ? new Set(shopFilter.adsetIds || [])
                       : new Set(shopFilter.campaignIds || []);
      base = base.filter(r => allowedSet.has(r.id));
    }
    return base;
  }, [selectedByLevel, campaigns, adSets, ads, visibleCampaignIds, shopFilter, dataSource]);

  // ── Aggregate stats จาก effective scope ─────────────────
  const stats = React.useMemo(() => {
    let spend = 0, impressions = 0, reach = 0, clicks = 0, revenue = 0, orders = 0, messages = 0;
    let pendingRevenue = 0, pendingOrders = 0, cancelledRevenue = 0, cancelledOrders = 0;
    let needsAttention = 0;
    const revKey = dataSource === "meta" ? "revenueMeta" : "revenuePeriod";
    const ordKey = dataSource === "meta" ? "ordersMeta"  : "ordersPeriod";
    effectiveScope.forEach(r => {
      spend       += r.spendPeriod        || 0;
      impressions += r.impressionsPeriod  || 0;
      reach       += r.reachPeriod        || 0;
      clicks      += r.clicksPeriod       || 0;  // ⭐ link_clicks
      revenue     += r[revKey]        || 0;
      orders      += r[ordKey]        || 0;
      messages    += r.messagesPeriod     || 0;
      // ⭐ Pending/Cancelled — มีเฉพาะ Real Order side (ERP)
      pendingRevenue   += r.pendingRevenue   || 0;
      pendingOrders    += r.pendingOrders    || 0;
      cancelledRevenue += r.cancelledRevenue || 0;
      cancelledOrders  += r.cancelledOrders  || 0;
      const rRoas = (r.spendPeriod > 0) ? ((r[revKey] || 0) / r.spendPeriod) : 0;
      if ((r.spendPeriod > 0 && rRoas < 1.5) || (r.alerts || []).length > 0) needsAttention++;
    });
    const scope = selectedByLevel.ad.size ? "ad" : selectedByLevel.adset.size ? "adset" : selectedByLevel.campaign.size ? "campaign" : "all";
    const scopeIds = scope === "all" ? null : effectiveScope.map(r => r.id);
    return {
      spend, impressions, reach, clicks, revenue, orders, messages,
      pendingRevenue, pendingOrders, cancelledRevenue, cancelledOrders,
      ctr:    impressions > 0 ? (clicks / impressions) * 100 : 0,  // link CTR
      roas:   spend > 0 ? revenue / spend : 0,
      cpm:    impressions > 0 ? (spend / impressions) * 1000 : 0,
      cpc:    clicks > 0 ? spend / clicks : 0,                     // CPC จาก link_clicks
      aov:    orders > 0 ? revenue / orders : 0,
      frequency: reach > 0 ? impressions / reach : 0,
      count:  effectiveScope.length,
      needsAttention,
      dataSource,
      scope,
      scopeIds,   // ⭐ entity IDs (campaign/adset/ad) ที่ user เลือก — ส่งให้ CustomerBreakdown filter
      entityName: effectiveScope.length === 1 ? effectiveScope[0]?.name : null,
    };
  }, [effectiveScope, selectedByLevel, dataSource]);

  // push stats ขึ้น Dashboard
  React.useEffect(() => { onStatsChange?.(stats); }, [stats]);
  React.useEffect(() => { onLoadingChange?.(fetching); }, [fetching]);

  // ── derive rows based on active level (พ่วง cascade) ──
  let rows = [], levelLabel = "แคมเปญ";
  if (level === "campaign") {
    rows = campaigns.filter(c => visibleCampaignIds.has(c.id));
    levelLabel = "แคมเปญ";
  } else if (level === "adset") {
    rows = adSets.filter(s => visibleAdSetIds.has(s.id));
    levelLabel = "Ad Set";
  } else if (level === "ad") {
    rows = ads.filter(a => visibleAdIds.has(a.id));
    levelLabel = "Ad";
  }

  // ⭐ Shop Type filter — เฉพาะ Real Order mode (Meta side ไม่อิง orders)
  if (shopFilter && dataSource === "real") {
    if (level === "campaign") {
      const allowed = new Set(shopFilter.campaignIds || []);
      rows = rows.filter(r => allowed.has(r.id));
    } else if (level === "adset") {
      const allowed = new Set(shopFilter.adsetIds || []);
      rows = rows.filter(r => allowed.has(r.id));
    } else if (level === "ad") {
      const allowed = new Set(shopFilter.adIds || []);
      rows = rows.filter(r => allowed.has(r.id));
    }
  }

  // Default order: ACTIVE rows first → id desc
  const statusRank = (r) => {
    const s = (r.effectiveStatus || r.status || "").toUpperCase();
    return s === "ACTIVE" ? 0 : s === "" ? 2 : 1;
  };
  const defaultOrder = (a, b) => statusRank(a) - statusRank(b)
    || String(b.id || "").localeCompare(String(a.id || ""));

  // map sortKey → จริงๆ ต้องอ่าน field ไหน (swap ตาม dataSource)
  const resolveSortVal = (row, key) => {
    if (!row) return 0;
    if (key === "roas")       return dataSource === "meta" ? (row.roasMeta    || 0) : (row.roas       || 0);
    if (key === "revenuePeriod")  return dataSource === "meta" ? (row.revenueMeta || 0) : (row.revenuePeriod  || 0);
    if (key === "ordersPeriod" || key === "ordersMeta") {
      return dataSource === "meta" ? (row.ordersMeta || 0) : (row.ordersPeriod || 0);
    }
    if (key === "cpa") {
      const o = dataSource === "meta" ? (row.ordersMeta || 0) : (row.ordersPeriod || 0);
      return o > 0 ? (row.spendPeriod || 0) / o : 0;
    }
    return row[key] ?? 0;
  };

  const filtered = rows
    .filter(r => {
      const q = search.toLowerCase();
      return (!q || r.name.toLowerCase().includes(q));
    })
    .sort((a, b) => {
      if (!sortKey) return defaultOrder(a, b);
      const av = resolveSortVal(a, sortKey), bv = resolveSortVal(b, sortKey);
      if (typeof av === "string" || typeof bv === "string") {
        const cmp = String(av).localeCompare(String(bv));
        return sortDir === "asc" ? cmp : -cmp;
      }
      return sortDir === "asc" ? av - bv : bv - av;
    });

  const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
  const pageRows   = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);

  const totalSpend = filtered.reduce((s, r) => s + (r.spendPeriod || 0), 0);

  // ── Column totals/averages for the table footer row ──
  const colTotals = React.useMemo(() => {
    const t = {
      dailyBudget: 0, spend: 0, revenue: 0, orders: 0,
      impressions: 0, clicks: 0,
      count: filtered.length,
    };
    const revKey = dataSource === "meta" ? "revenueMeta" : "revenuePeriod";
    const ordKey = dataSource === "meta" ? "ordersMeta"  : "ordersPeriod";
    for (const r of filtered) {
      t.dailyBudget += r.dailyBudget       || 0;
      t.spend       += r.spendPeriod       || 0;
      t.revenue     += r[revKey]           || 0;
      t.orders      += r[ordKey]           || 0;
      t.impressions += r.impressionsPeriod || 0;
      t.clicks      += r.clicksPeriod      || 0;
    }
    return {
      ...t,
      cpa:  t.orders      > 0 ? t.spend / t.orders        : 0,
      ctr:  t.impressions > 0 ? (t.clicks / t.impressions) * 100 : 0,
      cpm:  t.impressions > 0 ? (t.spend / t.impressions) * 1000 : 0,
      roas: t.spend       > 0 ? t.revenue / t.spend       : 0,
    };
  }, [filtered, dataSource]);

  // helper: sync selection → filter ของ tab ลูก (ถ้ามี)
  // - ติ๊ก campaign → filterByLevel.adset
  // - ติ๊ก adset → filterByLevel.ad
  const childFilterKey = level === "campaign" ? "adset" : level === "adset" ? "ad" : null;

  // ── helpers: prune descendant selections เมื่อ uncheck parent ──
  // ตัวอย่าง: uncheck campaign C → ลบ ad sets ใน selection ที่อยู่ใน C, ลบ ads ใน selection ที่อยู่ใต้ ad sets เหล่านั้น
  const pruneDescendantsForRemovedParent = (parentLevel, removedIds) => {
    if (!removedIds.length) return;
    setSelectedByLevel(prev => {
      const next = { ...prev };
      if (parentLevel === "campaign") {
        // ลบ ad sets ที่อยู่ใต้ campaigns ที่ uncheck
        const orphanAdsetIds = adSets.filter(s => removedIds.includes(s.campaignId)).map(s => s.id);
        if (orphanAdsetIds.length) {
          next.adset = new Set([...next.adset].filter(id => !orphanAdsetIds.includes(id)));
          // และลบ ads ที่อยู่ใต้ ad sets เหล่านั้น
          const orphanAdIds = ads.filter(a => orphanAdsetIds.includes(a.adSetId)).map(a => a.id);
          if (orphanAdIds.length) {
            next.ad = new Set([...next.ad].filter(id => !orphanAdIds.includes(id)));
          }
        }
      } else if (parentLevel === "adset") {
        const orphanAdIds = ads.filter(a => removedIds.includes(a.adSetId)).map(a => a.id);
        if (orphanAdIds.length) {
          next.ad = new Set([...next.ad].filter(id => !orphanAdIds.includes(id)));
        }
      }
      return next;
    });
    // sync ตัด filter ของ child ที่ตามมาด้วย
    setFilterByLevel(prev => {
      const next = { ...prev };
      if (parentLevel === "campaign") {
        const orphanAdsetIds = adSets.filter(s => removedIds.includes(s.campaignId)).map(s => s.id);
        if (orphanAdsetIds.length) {
          next.ad = new Set([...next.ad].filter(id => !orphanAdsetIds.includes(id)));
        }
      }
      return next;
    });
  };

  // selection helpers — toggle checkbox + sync filter ของ child tab + prune descendant ถ้า uncheck
  const toggleSel = (id) => {
    const isAdding = !selectedByLevel[level].has(id);
    setSelectedByLevel(prev => {
      const next = new Set(prev[level]);
      isAdding ? next.add(id) : next.delete(id);
      return { ...prev, [level]: next };
    });
    if (childFilterKey) {
      setFilterByLevel(prev => {
        const next = new Set(prev[childFilterKey]);
        isAdding ? next.add(id) : next.delete(id);
        return { ...prev, [childFilterKey]: next };
      });
    }
    // ถ้า uncheck (ไม่ใช่ add) → prune descendant selection ที่อยู่ใต้ id นี้
    if (!isAdding && level !== "ad") {
      pruneDescendantsForRemovedParent(level, [id]);
    }
  };

  // shift+click range select — เลือกตั้งแต่ anchor (ครั้งก่อน) ถึง id ปัจจุบัน (ทั้ง 2 ทิศ)
  // - ถ้า anchor selected อยู่ → "เลือก" ช่วงนั้นเพิ่ม
  // - ถ้า anchor unselected → "ยกเลิกเลือก" ช่วงนั้น
  const rangeSelect = (id) => {
    const anchorId = lastClickedByLevel[level];
    if (!anchorId || anchorId === id) { toggleSel(id); return; }
    const ids = pageRows.map(r => r.id);
    const i0 = ids.indexOf(anchorId);
    const i1 = ids.indexOf(id);
    if (i0 === -1 || i1 === -1) { toggleSel(id); return; }
    const [lo, hi] = i0 < i1 ? [i0, i1] : [i1, i0];
    const rangeIds = ids.slice(lo, hi + 1);
    // ใช้สถานะของ anchor เป็นตัวกำหนด: ถ้า anchor เคยถูก select → set range เป็น selected ทุกตัว, ไม่งั้น deselect
    const shouldSelect = selectedByLevel[level].has(anchorId);
    setSelectedByLevel(prev => {
      const next = new Set(prev[level]);
      if (shouldSelect) rangeIds.forEach(x => next.add(x));
      else              rangeIds.forEach(x => next.delete(x));
      return { ...prev, [level]: next };
    });
    if (childFilterKey) {
      setFilterByLevel(prev => {
        const next = new Set(prev[childFilterKey]);
        if (shouldSelect) rangeIds.forEach(x => next.add(x));
        else              rangeIds.forEach(x => next.delete(x));
        return { ...prev, [childFilterKey]: next };
      });
    }
    // ถ้า deselect ช่วง → prune descendant ของทุก id ที่ถูก deselect
    if (!shouldSelect && level !== "ad") {
      pruneDescendantsForRemovedParent(level, rangeIds);
    }
  };

  // ตัวจัดการคลิกแถว — รองรับทั้งคลิกปกติและ shift+click
  const handleRowClick = (id, evt) => {
    if (evt?.shiftKey) {
      rangeSelect(id);
    } else {
      toggleSel(id);
    }
    setLastClickedByLevel(p => ({ ...p, [level]: id }));
  };
  // ติ๊ก "เลือกทั้งหมด" บน header → toggle เฉพาะ ids ใน "page นี้" (ไม่ทับ selection ของ page อื่น)
  const toggleAll = () => {
    const ids = pageRows.map(r => r.id);
    if (!ids.length) return;
    const allOnPage = ids.every(id => selected.has(id));
    setSelectedByLevel(prev => {
      const next = new Set(prev[level]);
      if (allOnPage) ids.forEach(id => next.delete(id));
      else           ids.forEach(id => next.add(id));
      return { ...prev, [level]: next };
    });
    if (childFilterKey) {
      setFilterByLevel(prev => {
        const next = new Set(prev[childFilterKey]);
        if (allOnPage) ids.forEach(id => next.delete(id));
        else           ids.forEach(id => next.add(id));
        return { ...prev, [childFilterKey]: next };
      });
    }
    // ถ้า uncheck all บน page นี้ → prune descendants ของ ids เหล่านี้
    if (allOnPage && level !== "ad") {
      pruneDescendantsForRemovedParent(level, ids);
    }
  };

  // header state: ทั้งหมด/บางส่วน/ว่าง (สำหรับ indeterminate)
  const headerCheckState = React.useMemo(() => {
    if (pageRows.length === 0) return "empty";
    const ids = pageRows.map(r => r.id);
    const checked = ids.filter(id => selected.has(id)).length;
    if (checked === 0) return "none";
    if (checked === ids.length) return "all";
    return "some";
  }, [pageRows, selected]);

  const switchLevel = (newLevel) => {
    setLevel(newLevel);
    setSearch("");
  };

  // ล้าง selection + cascade ลงทุก descendant
  const clearOwnSelection = () => {
    if (level === "campaign") {
      setSelectedByLevel({ campaign: new Set(), adset: new Set(), ad: new Set() });
      setFilterByLevel({ adset: new Set(), ad: new Set() });
    } else if (level === "adset") {
      setSelectedByLevel(p => ({ ...p, adset: new Set(), ad: new Set() }));
      setFilterByLevel(p => ({ ...p, ad: new Set() }));
      // ไม่กระทบ Campaigns tab (parent)
    } else if (level === "ad") {
      setSelectedByLevel(p => ({ ...p, ad: new Set() }));
    }
  };

  // ── Global filter state — ใช้กับปุ่ม "ล้างทั้งหมด" ────────
  const anyFilterActive = React.useMemo(() => {
    if (selectedByLevel.campaign.size || selectedByLevel.adset.size || selectedByLevel.ad.size) return true;
    if (statusFilterByLevel.campaign !== "ALL" || statusFilterByLevel.adset !== "ALL" || statusFilterByLevel.ad !== "ALL") return true;
    if (search.trim()) return true;
    if (sortByLevel.campaign.key !== null || sortByLevel.adset.key !== null || sortByLevel.ad.key !== null) return true;
    return false;
  }, [selectedByLevel, statusFilterByLevel, search, sortByLevel]);

  const clearAllFilters = () => {
    setSelectedByLevel({ campaign: new Set(), adset: new Set(), ad: new Set() });
    setFilterByLevel({ adset: new Set(), ad: new Set() });
    setStatusFilterByLevel({ campaign: "ALL", adset: "ALL", ad: "ALL" });
    setLastClickedByLevel({ campaign: null, adset: null, ad: null });
    setSearch("");
    setSortByLevel({
      campaign: { key: null, dir: "desc" },
      adset:    { key: null, dir: "desc" },
      ad:       { key: null, dir: "desc" },
    });
  };

  // helper: ดึงชื่อจาก ids ที่ติ๊กไว้บน tab ปัจจุบัน (banner ของ level ตัวเอง)
  const ownSelectionLabel = React.useMemo(() => {
    const sel = selectedByLevel[level];
    if (!sel.size) return null;
    const labelMap = { campaign: "Campaign", adset: "Ad Set", ad: "Ad" };
    const source = level === "campaign" ? campaigns : level === "adset" ? adSets : ads;
    const names = source.filter(r => sel.has(r.id)).map(r => r.name);
    return { label: labelMap[level], count: sel.size, names };
  }, [level, selectedByLevel, campaigns, adSets, ads]);


  return (
    <div className="screen" style={{ paddingBottom: selected.size ? 72 : 0 }}>

      {/* Header (omitted when embedded — Dashboard renders its own) */}
      {!hideHeader && (
        <div className="screen-head">
          <div style={{ minWidth: 0 }}>
            <h1 className="page-title">Campaigns</h1>
          </div>
          <div className="head-actions" style={{ flexWrap: "wrap", gap: 6 }}>
            <div className="range-tabs">
              {ranges.map(r => (
                <button key={r.k} className={timeRange === r.k && !customApplied ? "active" : ""}
                  onClick={() => { setTimeRange(r.k); clearCustom(); }}>{r.l}</button>
              ))}
              <div style={{ position: "relative" }}>
                <button className={showCustom || customApplied ? "active" : ""}
                  onClick={() => setShowCustom(v => !v)}
                  style={{ display: "flex", alignItems: "center", gap: 5 }}>
                  <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2V6M8 2V6M3 10H21"/></svg>
                  {customSince && customUntil
                    ? `${new Date(customSince+'T00:00:00').toLocaleDateString('th-TH',{day:'numeric',month:'short'})} – ${new Date(customUntil+'T00:00:00').toLocaleDateString('th-TH',{day:'numeric',month:'short'})}`
                    : "กำหนดเอง"}
                </button>
                {showCustom && (
                  <DateRangePicker since={customSince} until={customUntil}
                    onChange={(s, e) => applyCustom(s, e)}
                    onClose={() => setShowCustom(false)} />
                )}
              </div>
            </div>
          </div>
        </div>
      )}


      {/* Selection banner — แสดงเมื่อ tab ปัจจุบันมี selection (ของ level ตัวเอง) */}
      {/* Selection banner — always visible (เพื่อกัน UI ขยับ); empty state = สีเทาดำ */}
      {(() => {
        const levelLabelMap = { campaign: "Campaign", adset: "Ad Set", ad: "Ad" };
        const lbl = levelLabelMap[level];
        const hasSelection = !!ownSelectionLabel;
        const accentBg = "var(--accent-bg, #eef2ff)";
        const emptyBg  = "color-mix(in srgb, var(--ink-faint) 8%, transparent)";
        const accentBorder = "color-mix(in srgb, var(--accent) 25%, transparent)";
        const emptyBorder  = "var(--border)";
        return (
          <div style={{
            display: "flex", alignItems: "center", gap: 10,
            padding: "8px 12px", marginBottom: 10,
            background: hasSelection ? accentBg : emptyBg,
            border: `1px solid ${hasSelection ? accentBorder : emptyBorder}`,
            borderRadius: 8, fontSize: 12,
            color: hasSelection ? "var(--accent)" : "var(--ink-faint)",
            transition: "background 0.15s, border-color 0.15s",
          }}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ flexShrink: 0 }}>
              {hasSelection
                ? <path d="M9 12L11 14L15 10M21 12A9 9 0 113 12A9 9 0 0121 12Z"/>
                : <circle cx="12" cy="12" r="9"/>}
            </svg>
            <span style={{ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
              {hasSelection ? (
                <>
                  เลือก <strong>{ownSelectionLabel.count} {ownSelectionLabel.label}</strong>:
                  <span style={{ marginLeft: 6, color: "var(--ink)", fontWeight: 500 }} title={ownSelectionLabel.names.join(", ")}>
                    {ownSelectionLabel.names.slice(0, 2).map(n => n.length > 28 ? n.slice(0, 28) + "…" : n).join(", ")}
                    {ownSelectionLabel.names.length > 2 && ` +${ownSelectionLabel.names.length - 2}`}
                  </span>
                  {childFilterKey && (
                    <span className="muted small" style={{ marginLeft: 10, fontWeight: 400, color: "var(--ink-faint)" }}>
                      · จะใช้กรอง {childFilterKey === "adset" ? "Ad Sets" : "Ads"}
                    </span>
                  )}
                </>
              ) : (
                <>
                  ยังไม่ได้เลือก <strong>{lbl}</strong> <span style={{ marginLeft: 4, opacity: 0.6 }}>— คลิกแถวเพื่อเลือก (Shift+คลิก = เลือกหลาย)</span>
                </>
              )}
            </span>
            <button onClick={hasSelection ? clearOwnSelection : undefined}
              disabled={!hasSelection}
              title={hasSelection ? `ล้าง ${ownSelectionLabel.label} ที่เลือก` : ""}
              style={{
                border: "none", background: "transparent",
                color: hasSelection ? "var(--accent)" : "var(--ink-faint)",
                cursor: hasSelection ? "pointer" : "default",
                fontSize: 11, fontWeight: 600, padding: "3px 8px", borderRadius: 5,
                whiteSpace: "nowrap", opacity: hasSelection ? 1 : 0.5,
              }}>
              ล้าง {lbl}
            </button>
          </div>
        );
      })()}

      {/* Tabs + Toolbar (อยู่ row เดียวกัน) */}
      <div style={{
        display: "flex", alignItems: "center", gap: 8,
        marginBottom: 12, borderBottom: "1px solid var(--border)",
        flexWrap: "wrap", paddingBottom: 0,
      }}>
        {/* FB-style Level Tabs */}
        {[
          { k: "campaign", l: "Campaigns", selCount: selectedByLevel.campaign.size },
          { k: "adset",    l: "Ad Sets",   selCount: selectedByLevel.adset.size },
          { k: "ad",       l: "Ads",       selCount: selectedByLevel.ad.size },
        ].map(t => (
          <button key={t.k} onClick={() => switchLevel(t.k)}
            style={{
              padding: "10px 14px", border: "none", background: "transparent",
              borderBottom: level === t.k ? "2px solid var(--accent)" : "2px solid transparent",
              color: level === t.k ? "var(--accent)" : "var(--ink-faint)",
              fontSize: 13, fontWeight: level === t.k ? 600 : 500, cursor: "pointer",
              marginBottom: -1, display: "flex", alignItems: "center", gap: 6,
            }}>
            {t.l}
            {t.selCount > 0 && (
              <span style={{
                background: level === t.k ? "var(--accent)" : "var(--ink-faint)",
                color: "#fff", borderRadius: 10, padding: "1px 7px",
                fontSize: 10, fontWeight: 600, lineHeight: "14px",
              }}>
                {t.selCount}
              </span>
            )}
          </button>
        ))}

        {/* Separator */}
        <span style={{ width: 1, height: 18, background: "var(--border)", margin: "0 4px" }} />

        {/* Search */}
        <div style={{ position: "relative", flex: "1 1 180px", maxWidth: 260, marginBottom: 6 }}>
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--ink-faint)" strokeWidth="2"
            style={{ position: "absolute", left: 9, top: "50%", transform: "translateY(-50%)", pointerEvents: "none" }}>
            <circle cx="11" cy="11" r="8"/><path d="M21 21L16.65 16.65"/>
          </svg>
          <input value={search} onChange={e => setSearch(e.target.value)}
            placeholder={`ค้นหา${levelLabel}…`}
            style={{ width: "100%", paddingLeft: 28, paddingRight: 8, paddingTop: 6, paddingBottom: 6, border: "1px solid var(--border)", borderRadius: 7, fontSize: 13, background: "var(--bg-elevated)", color: "var(--ink)", boxSizing: "border-box" }} />
        </div>

        {/* Status filter */}
        <div className="seg" style={{ marginBottom: 6 }}>
          {[["ALL","ทั้งหมด"],["ACTIVE","Active"],["PAUSED","Paused"]].map(([k,l]) => (
            <button key={k} className={statusFilter === k ? "active" : ""} onClick={() => setStatusFilter(k)}>{l}</button>
          ))}
        </div>

        {/* Clear all */}
        <button
          onClick={anyFilterActive ? clearAllFilters : undefined}
          disabled={!anyFilterActive}
          title={anyFilterActive ? "ล้างทุก filter (select + status + search) ทุก level" : "ไม่มี filter ให้ล้าง"}
          style={{
            marginLeft: "auto",
            marginBottom: 6,
            display: "flex", alignItems: "center", gap: 4,
            padding: "5px 10px",
            border: `1px solid ${anyFilterActive ? "var(--danger, #dc2626)" : "var(--border)"}`,
            borderRadius: 6,
            background: anyFilterActive ? "color-mix(in srgb, var(--danger, #dc2626) 8%, transparent)" : "transparent",
            color: anyFilterActive ? "var(--danger, #dc2626)" : "var(--ink-faint)",
            cursor: anyFilterActive ? "pointer" : "default",
            fontSize: 11, fontWeight: 600,
            opacity: anyFilterActive ? 1 : 0.55,
            transition: "all 0.15s",
          }}>
          <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
            <path d="M3 6H21M19 6V20A2 2 0 0117 22H7A2 2 0 015 20V6M8 6V4A2 2 0 0110 2H14A2 2 0 0116 4V6M10 11V17M14 11V17"/>
          </svg>
          ล้างทั้งหมด
        </button>
      </div>

      {/* Table — wrap ใน scroll container; fixed cols จะ sticky ทางซ้าย */}
      <div className="hierarchy-table-scroll" style={{ "--name-w": `${nameColW}px` }}>
        {/* Head */}
        <div className="h-row h-head">
          <div className="h-cell">
            <input
              type="checkbox"
              checked={headerCheckState === "all"}
              ref={el => { if (el) el.indeterminate = headerCheckState === "some"; }}
              onChange={toggleAll}
              title={headerCheckState === "all" ? "ยกเลิกเลือกทั้งหน้านี้" : "เลือกทั้งหน้านี้"}
              style={{ cursor: "pointer" }} />
          </div>
          <div className="h-cell name sortable" onClick={() => onSort("name")} style={{ cursor: "pointer", userSelect: "none" }}>
            ชื่อ <SortIcon k="name" />
            <span ref={resizerRef} className="col-resizer" onMouseDown={onResizeStart} title="ลากเพื่อปรับขนาด" />
          </div>
          <Th k="dailyBudget"   label="งบ/วัน" />
          <Th k="spendPeriod"       label="Spend" />
          <Th k={dataSource === "meta" ? "revenueMeta" : "revenuePeriod"} label="รายได้" />
          <Th k={dataSource === "meta" ? "ordersMeta" : "ordersPeriod"} label="ซื้อ" />
          <Th k="cpa"           label="CPA" />
          {/* scrollable cols */}
          <Th k="impressionsPeriod" label="Impr." />
          <Th k="ctr"           label="CTR" />
          <Th k="clicksPeriod"      label="Clicks" />
          <Th k="cpm"           label="CPM" />
          <div className="h-cell sortable" onClick={() => onSort("aiStatus")} style={{ cursor: "pointer", userSelect: "none" }}>
            AI <SortIcon k="aiStatus" />
          </div>
          {/* sticky right: ROAS + Trend */}
          <Th k="roas"          label="ROAS" />
          <div className="h-cell" style={{ justifyContent: "center" }}>Trend</div>
        </div>

        {/* Loading */}
        {fetching && pageRows.length === 0 && (
          <div style={{ padding: "48px 0", textAlign: "center", color: "var(--ink-faint)" }}>
            <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2.5"
              style={{ marginBottom: 10, animation: "spin 0.8s linear infinite" }}>
              <path d="M21 12A9 9 0 1112 3" />
            </svg>
            <div>กำลังโหลดข้อมูล…</div>
          </div>
        )}

        {/* Empty (เฉพาะตอนโหลดเสร็จแล้วและไม่มีข้อมูล) */}
        {!fetching && pageRows.length === 0 && (
          <div style={{ padding: "48px 0", textAlign: "center", color: "var(--ink-faint)" }}>
            <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ marginBottom: 8, opacity: 0.35 }}><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9H21M9 21V9"/></svg>
            <div>{search ? `ไม่พบ "${search}"` : `ไม่มีข้อมูล${levelLabel}`}</div>
          </div>
        )}

        {/* Rows */}
        {pageRows.map(r => {
          // ใช้ displayStatus (รวม STALE/ARCHIVED logic แล้ว) ถ้ามี — ไม่งั้น fallback
          const status = r.displayStatus || r.effectiveStatus || r.status || "PAUSED";
          const isSelected = selected.has(r.id);
          const roasVal = dataSource === "meta" ? (r.roasMeta || 0) : (r.roas || 0);
          const revVal  = dataSource === "meta" ? (r.revenueMeta || 0) : (r.revenuePeriod || 0);

          return (
            <div key={r.id} className={`h-row h-level-1${isSelected ? " h-selected" : ""}`}
              onClick={(e) => handleRowClick(r.id, e)}>

              {/* Checkbox */}
              <div className="h-cell" onClick={e => { e.stopPropagation(); handleRowClick(r.id, e); }}>
                <input type="checkbox" checked={isSelected}
                  onChange={() => {}}
                  style={{ cursor: "pointer", pointerEvents: "none" }} />
              </div>

              {/* Name */}
              <div className="h-cell name">
                <StatusBadge status={status} />
                <div style={{ minWidth: 0, flex: 1 }}>
                  <span className="h-name" title={r.name}
                    style={{ color: "var(--ink)", fontWeight: 500, display: "block" }}>
                    {r.name}
                  </span>
                  {r.objective && <div className="muted small">{r.objective.replace("OUTCOME_", "")}</div>}
                </div>
              </div>

              {/* Sticky-left metric cols */}
              <div className="h-cell num">{r.dailyBudget ? fmt.money(r.dailyBudget) : <span className="muted">—</span>}</div>
              <div className="h-cell num">{money(r.spendPeriod)}</div>
              <div className="h-cell num">{revVal > 0 ? fmt.money(revVal) : <span className="muted">—</span>}</div>
              {(() => {
                const ordersVal = dataSource === "meta" ? (r.ordersMeta || 0) : (r.ordersPeriod || 0);
                const cpaVal = ordersVal > 0 ? r.spendPeriod / ordersVal : 0;
                return <>
                  <div className="h-cell num" title="จำนวนซื้อ">{ordersVal > 0 ? num(ordersVal) : <span className="muted">—</span>}</div>
                  <div className="h-cell num" title="ค่าโฆษณา / ซื้อ">{cpaVal > 0 ? fmt.money(cpaVal) : <span className="muted">—</span>}</div>
                </>;
              })()}

              {/* Scrollable middle cols */}
              <div className="h-cell num">{num(r.impressionsPeriod)}</div>
              <div className="h-cell num">{pct(r.ctr)}</div>
              <div className="h-cell num">{num(r.clicksPeriod)}</div>
              <div className="h-cell num">{money(r.cpm)}</div>
              <div className="h-cell">
                {r.aiStatus ? <AiStatusChip status={r.aiStatus} /> : <span className="muted small">—</span>}
              </div>

              {/* Sticky-right cols: ROAS + Trend */}
              <div className="h-cell num">
                {r.spendPeriod > 0
                  ? <span className={`roas-pill ${roasVal >= 3 ? "good" : roasVal >= 2 ? "ok" : roasVal >= 1.5 ? "warn" : "bad"}`}>{roasVal.toFixed(2)}×</span>
                  : <span className="muted">—</span>}
              </div>
              {(() => {
                // คำนวณ ROAS รายวัน = revenue_day / spend_day (ตาม dataSource toggle)
                const spends = r.trendDailySpend || [];
                const revs   = dataSource === "meta" ? (r.trendDailyMetaRev || []) : (r.trendDailyOurRev || []);
                const roasArr = spends.map((s, i) => (s > 0 ? (revs[i] || 0) / s : 0));
                const hasData = roasArr.length > 0 && roasArr.some(v => v > 0);
                const maxRoas = hasData ? Math.max(...roasArr) : 0;
                return (
                  <div className="h-cell" style={{ justifyContent: "center" }}
                    title={hasData
                      ? `ROAS รายวัน ${roasArr.length} จุด (ซ้าย=เก่า → ขวา=ใหม่)\nMin: ${Math.min(...roasArr).toFixed(2)}× · Max: ${maxRoas.toFixed(2)}×\nสเกล: 0–${Math.max(10, Math.ceil(maxRoas))}`
                      : "ไม่มีข้อมูล ROAS trend"}>
                    {hasData
                      ? <Sparkline data={roasArr} yMin={0} yMax={10} refLine={1}
                          color={maxRoas >= 2 ? "var(--success)" : maxRoas >= 1.5 ? "var(--warning)" : "var(--danger)"} />
                      : <span className="muted small">—</span>}
                  </div>
                );
              })()}
            </div>
          );
        })}

        {/* Totals row — sticky bottom inside scroll, mirrors column structure */}
        {!fetching && filtered.length > 0 && (
          <div className="h-row" style={{
            position: "sticky", bottom: 0, zIndex: 4,
            background: "var(--bg-soft, var(--bg))",
            borderTop: "2px solid var(--border)",
            fontWeight: 600,
          }}>
            <div className="h-cell" />
            <div className="h-cell name" style={{ color: "var(--ink-faint)", fontStyle: "italic" }}>
              Σ Totals · {colTotals.count} {levelLabel}
            </div>
            <div className="h-cell" style={{ textAlign: "right", justifyContent: "flex-end" }}>
              {colTotals.dailyBudget > 0 ? fmt.money(colTotals.dailyBudget) : "—"}
            </div>
            <div className="h-cell" style={{ textAlign: "right", justifyContent: "flex-end" }}>
              {fmt.money(colTotals.spend)}
            </div>
            <div className="h-cell" style={{ textAlign: "right", justifyContent: "flex-end" }}>
              {fmt.money(colTotals.revenue)}
            </div>
            <div className="h-cell" style={{ textAlign: "right", justifyContent: "flex-end" }}>
              {fmt.num(colTotals.orders)}
            </div>
            <div className="h-cell" style={{ textAlign: "right", justifyContent: "flex-end" }} title="avg CPA">
              {colTotals.orders > 0 ? fmt.money(colTotals.cpa) : "—"}
            </div>
            <div className="h-cell" style={{ textAlign: "right", justifyContent: "flex-end" }}>
              {fmt.num(colTotals.impressions)}
            </div>
            <div className="h-cell" style={{ textAlign: "right", justifyContent: "flex-end" }} title="avg CTR">
              {colTotals.impressions > 0 ? fmt.pct(colTotals.ctr) : "—"}
            </div>
            <div className="h-cell" style={{ textAlign: "right", justifyContent: "flex-end" }}>
              {fmt.num(colTotals.clicks)}
            </div>
            <div className="h-cell" style={{ textAlign: "right", justifyContent: "flex-end" }} title="avg CPM">
              {colTotals.impressions > 0 ? fmt.money(colTotals.cpm) : "—"}
            </div>
            <div className="h-cell" />
            <div className="h-cell" style={{ textAlign: "right", justifyContent: "flex-end" }} title="avg ROAS">
              {colTotals.spend > 0 ? colTotals.roas.toFixed(2) + "×" : "—"}
            </div>
            <div className="h-cell" />
          </div>
        )}
      </div>

      {/* Footer bar — sticky bottom (count + pagination) */}
      <div style={{
        position: "sticky", bottom: 0, zIndex: 5,
        background: "var(--bg)",
        borderTop: "1px solid var(--border)",
        padding: "8px 12px",
        marginTop: 0,
        display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap",
        fontSize: 12,
      }}>
        <span className="muted">
          {filtered.length} {levelLabel}
          {totalSpend > 0 && <> · Spend <strong style={{ color: "var(--ink)" }}>{fmt.money(totalSpend)}</strong></>}
        </span>
        {totalPages > 1 && (
          <div style={{ display: "flex", alignItems: "center", gap: 4, marginLeft: "auto" }}>
            <button className="btn-ghost-sm" disabled={page === 1} onClick={() => setPage(p => p - 1)}>‹</button>
            {(() => {
              // โชว์ page numbers แบบ smart: 1...current-1, current, current+1...total
              const pages = [];
              const last = totalPages;
              const cur = page;
              const add = (p) => pages.push(p);
              add(1);
              if (cur > 3) pages.push("...");
              for (let i = Math.max(2, cur - 1); i <= Math.min(last - 1, cur + 1); i++) add(i);
              if (cur < last - 2) pages.push("...");
              if (last > 1) add(last);
              return pages.map((p, i) =>
                p === "..." ? <span key={"e" + i} className="muted small" style={{ padding: "0 4px" }}>…</span>
                : <button key={p} onClick={() => setPage(p)}
                    style={{ minWidth: 24, padding: "2px 6px", borderRadius: 5, border: "1px solid var(--border)", background: page === p ? "var(--accent)" : "transparent", color: page === p ? "#fff" : "var(--ink)", cursor: "pointer", fontSize: 11 }}>
                    {p}
                  </button>
              );
            })()}
            <button className="btn-ghost-sm" disabled={page === totalPages} onClick={() => setPage(p => p + 1)}>›</button>
            <span className="muted small" style={{ marginLeft: 8 }}>
              {(page - 1) * PAGE_SIZE + 1}–{Math.min(page * PAGE_SIZE, filtered.length)} / {filtered.length}
            </span>
          </div>
        )}
      </div>

      {/* Bulk Action Bar */}
      {selected.size > 0 && (
        <div style={{
          position: "fixed", bottom: 20, left: "50%", transform: "translateX(-50%)",
          background: "var(--ink)", color: "#fff", borderRadius: 12, padding: "10px 16px",
          display: "flex", alignItems: "center", gap: 10, boxShadow: "0 4px 24px rgba(0,0,0,.3)",
          zIndex: 800, fontSize: 13, whiteSpace: "nowrap",
        }}>
          <span style={{ fontWeight: 600 }}>เลือกแล้ว {selected.size} รายการ</span>
          <span style={{ width: 1, height: 18, background: "rgba(255,255,255,.2)" }} />
          {[
            { icon: "M10 9V5A1 1 0 019 4H7A1 1 0 006 5V19A1 1 0 007 20H17A1 1 0 0018 19V5A1 1 0 0017 4H15A1 1 0 0014 5V9", label: "Pause" },
            { icon: "M6 4L20 12L6 20Z", label: "Resume" },
            { icon: "M8 2H16M8 22H16M3 8H7M17 8H21M3 16H7M17 16H21M7 2V22M17 2V22", label: "Duplicate" },
          ].map(({ icon, label }) => (
            <button key={label} style={{ background: "rgba(255,255,255,.12)", border: "none", color: "#fff", padding: "5px 10px", borderRadius: 6, cursor: "pointer", fontSize: 12, display: "flex", alignItems: "center", gap: 4 }}>
              <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d={icon}/></svg>
              {label}
            </button>
          ))}
          <button style={{ background: "var(--accent)", border: "none", color: "#fff", padding: "5px 10px", borderRadius: 6, cursor: "pointer", fontSize: 12, display: "flex", alignItems: "center", gap: 4 }}>
            <svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L13.5 8.5L20 10L13.5 11.5L12 18L10.5 11.5L4 10L10.5 8.5Z"/></svg>
            AI Optimize
          </button>
          <button onClick={() => setSelected(new Set())} style={{ background: "transparent", border: "none", color: "rgba(255,255,255,.6)", cursor: "pointer", fontSize: 16, padding: "0 4px" }}>✕</button>
        </div>
      )}
    </div>
  );
};

/* ================ AD DETAIL · True ROAS ================ */
const AdDetail = ({ ad, onBack }) => {
  const adSet = window.MOCK.adSets.find(a => a.id === ad.adSetId);
  const campaign = window.MOCK.campaigns.find(c => c.id === adSet?.campaignId);
  const creative = window.MOCK.creatives.find(c => c.id === ad.creative);
  const relatedOrders = window.MOCK.orders.filter(o => o.adId === ad.id);
  const totalRev = relatedOrders.reduce((s, o) => s + o.total, 0);
  const totalCogs = totalRev * 0.26;
  const fbReported = ad.spendPeriod * ad.roas * 0.72; // FB underreports

  return (
    <div className="screen">
      <button className="back-btn" onClick={onBack}>
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M14 6L8 12L14 18"/></svg>
        กลับ
      </button>
      <div className="screen-head">
        <div>
          <div className="breadcrumb">{campaign?.name} → {adSet?.name.substring(0, 30)}…</div>
          <h1 className="page-title">{ad.name}</h1>
          <div style={{ display: "flex", gap: 12, marginTop: 8, alignItems: "center" }}>
            <StatusBadge status={ad.status} />
            <span className={`format-tag fmt-${ad.format}`}>{ad.format.toUpperCase()}</span>
            {ad.winner && <span className="winner-tag">★ Winner</span>}
            <span className="muted small">FB ID: 23851{ad.id}</span>
          </div>
        </div>
        <div className="head-actions">
          <button className="btn-ghost">Duplicate</button>
          <button className="btn-ghost">Edit</button>
          <button className="btn-primary">
            <svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L13.5 8.5L20 10L13.5 11.5L12 18L10.5 11.5L4 10L10.5 8.5Z"/></svg>
            Ask AI
          </button>
        </div>
      </div>

      <div className="ad-detail-grid">
        <div className="ad-preview-col">
          <div className="ad-preview-card">
            <div className="muted small" style={{ marginBottom: 8 }}>Preview · Facebook Feed</div>
            <div className="fb-preview">
              <div className="fb-head">
                <div className="fb-avatar">P</div>
                <div>
                  <div style={{ fontWeight: 600, fontSize: 12 }}>Wandee</div>
                  <div style={{ fontSize: 10, color: "var(--ink-faint)" }}>Sponsored · <svg width="9" height="9" viewBox="0 0 24 24" fill="currentColor" style={{ display: "inline" }}><circle cx="12" cy="12" r="10"/></svg></div>
                </div>
              </div>
              <div className="fb-copy">{creative?.body}</div>
              <div className={`fb-media media-${creative?.color}`}>
                {ad.format === "video" && (
                  <div className="play-overlay">
                    <svg width="32" height="32" viewBox="0 0 24 24" fill="white"><path d="M6 4L20 12L6 20Z"/></svg>
                  </div>
                )}
                <span className="media-placeholder-text">{ad.format.toUpperCase()} · 1080 × 1080</span>
              </div>
              <div className="fb-cta">
                <div>
                  <div style={{ fontSize: 11, color: "var(--ink-faint)" }}>wandee.co.th</div>
                  <div style={{ fontWeight: 600, fontSize: 13 }}>{creative?.body.substring(0, 32)}…</div>
                </div>
                <button className="fb-cta-btn">{creative?.cta.replace(/_/g, " ")}</button>
              </div>
            </div>
          </div>

          <div className="creative-meta">
            <div><span className="muted">Format</span><strong>{creative?.format}</strong></div>
            <div><span className="muted">CTA</span><strong>{creative?.cta}</strong></div>
            <div><span className="muted">Body copy</span><strong>{creative?.body}</strong></div>
            <div><span className="muted">Performance score</span><strong>{creative?.performance}/5.0</strong></div>
          </div>
        </div>

        <div className="ad-stats-col">
          <div className="roas-compare">
            <div className="roas-card fb">
              <div className="roas-label">Facebook ROAS</div>
              <div className="roas-value">{(ad.roas * 0.72).toFixed(2)}×</div>
              <div className="roas-sub">{fmt.money(fbReported)} reported</div>
            </div>
            <div className="roas-arrow">→</div>
            <div className="roas-card true">
              <div className="roas-label">
                <svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L13.5 8.5L20 10L13.5 11.5L12 18L10.5 11.5L4 10L10.5 8.5Z"/></svg>
                True ROAS (joined Order)
              </div>
              <div className="roas-value good">{ad.roas.toFixed(2)}×</div>
              <div className="roas-sub good">{fmt.money(ad.spendPeriod * ad.roas)} actual · +{Math.round((ad.roas / (ad.roas * 0.72) - 1) * 100)}%</div>
            </div>
          </div>

          <div className="metric-grid">
            <div className="metric"><span>Spend</span><strong>{fmt.money(ad.spendPeriod)}</strong></div>
            <div className="metric"><span>Impressions</span><strong>{fmt.num(ad.impressionsPeriod)}</strong></div>
            <div className="metric"><span>CTR</span><strong>{fmt.pct(ad.ctr)}</strong></div>
            <div className="metric"><span>CPM</span><strong>{fmt.money(ad.spendPeriod / ad.impressionsPeriod * 1000)}</strong></div>
            <div className="metric"><span>Orders</span><strong>{ad.ordersPeriod}</strong></div>
            <div className="metric"><span>AOV</span><strong>{fmt.money(ad.spendPeriod * ad.roas / Math.max(ad.ordersPeriod, 1))}</strong></div>
            <div className="metric"><span>COGS</span><strong>{fmt.money(totalCogs)}</strong></div>
            <div className="metric highlight"><span>กำไรสุทธิ</span><strong>{fmt.money(ad.spendPeriod * ad.roas - ad.spendPeriod - totalCogs)}</strong></div>
          </div>

          <div className="related-orders">
            <h3>Orders จากแอดนี้ <span className="muted small">({relatedOrders.length} ออเดอร์ · {fmt.money(totalRev)})</span></h3>
            <table className="data-table compact">
              <thead>
                <tr>
                  <th>Order</th>
                  <th>ลูกค้า</th>
                  <th>สินค้า</th>
                  <th className="right">Total</th>
                  <th>Touches</th>
                  <th>First seen</th>
                </tr>
              </thead>
              <tbody>
                {relatedOrders.map(o => (
                  <tr key={o.id}>
                    <td className="mono">{o.code}</td>
                    <td>{o.customer}</td>
                    <td className="truncate">{o.items.join(", ")}</td>
                    <td className="right num">{fmt.money(o.total)}</td>
                    <td><span className="touch-pill">{o.touches}</span></td>
                    <td className="muted small">{o.firstSeen}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      </div>
    </div>
  );
};

window.Dashboard = Dashboard;
window.CampaignsHierarchy = CampaignsHierarchy;
window.AdDetail = AdDetail;
window.StatusBadge = StatusBadge;
window.AiStatusChip = AiStatusChip;
window.Sparkline = Sparkline;
window.KpiCard = KpiCard;
window.fmt = fmt;
