/* report.jsx — หน้ารายงานสำหรับเปิดนำเสนอผู้บริหาร (ผู้ดูแล/ฝ่ายบัญชี ดูได้อย่างเดียว)
 * ยอดขออนุมัติรวม + กราฟเส้นเทียบรายเดือน + 10 อันดับ แล้วเจาะดูได้ 4 มุม:
 *   ตามฝ่าย / ตามค่าใช้จ่าย (รหัสบัญชี) / ตามหมวด / คำชี้แจงของทุกฝ่าย
 * กรองได้ตามฝ่าย ชื่อค่าใช้จ่าย และสถานะ (ต้องชี้แจง/ควรทบทวน) · เรียงได้ทุกคอลัมน์ · โหมดนำเสนอ · พิมพ์ · Excel
 * ใช้ข้อมูลชุดเดียวกับหน้าภาพรวม (API.adminAll) และกติกาจาก R.evaluateLine — ไม่คำนวณเกณฑ์เองในไฟล์นี้
 * กราฟวาดด้วย SVG เอง ไม่ใช้ไลบรารีเพิ่ม */

const REPORT_VIEWS = [
  { key: 'dept', label: 'ตามฝ่าย', unit: 'ฝ่าย' },
  { key: 'acct', label: 'ตามค่าใช้จ่าย (รหัสบัญชี)', unit: 'รหัส' },
  { key: 'cat', label: 'ตามหมวดค่าใช้จ่าย', unit: 'หมวด' },
  { key: 'note', label: 'คำชี้แจง', unit: 'รายการ' },
];
const NO_CAT = 'ไม่ระบุหมวด';
const FLAG_RANK = { red: 2, yellow: 1, none: 0 };
// สีของเส้น/แท่งในกราฟ: Budget ฟ้า · Actual และ Forecast เขียวน้ำทะเล · ยอดขออนุมัติ เขียว (ชุดเดียวกับกล่องสรุป)
const CHART_COLORS = { B: '#6f8fd0', A: '#0b8f9c', R: '#22a878' };

/** รวมยอดของชุดบรรทัด [{ l, ev }] · ส่วนต่างและ % มีความหมายเมื่อกรอกครบทุกบรรทัดแล้ว (complete) */
function aggregateLines(items) {
  const s = { n: 0, filled: 0, confirmed: 0, B: 0, A: 0, P: 0, R: 0, red: 0, yellow: 0, noteMissing: 0, over: 0 };
  for (const { l, ev } of items) {
    s.n++; s.B += ev.B; s.A += ev.A; s.P += ev.P || 0; s.R += ev.R;
    if (ev.filled) s.filled++;
    if (l.confirmed) s.confirmed++;
    if (ev.flag === 'red') { s.red++; s.over += Math.max(0, ev.diff); }
    if (ev.flag === 'yellow') s.yellow++;
    if (ev.noteMissing) s.noteMissing++;
  }
  ['B', 'A', 'R', 'over'].forEach((k) => { s[k] = R.round2(s[k]); });
  s.complete = s.n > 0 && s.filled === s.n;
  s.diff = R.round2(s.R - s.B);
  s.pct = s.B > 0 ? (s.diff / s.B) * 100 : null;
  return s;
}

/** เรียงโดยค่าว่าง (ยังไม่กรอก/ยังเทียบไม่ได้) อยู่ท้ายเสมอ · ค่าเท่ากันเรียงตามยอดขออนุมัติมากไปน้อย แล้วตามชื่อ */
function reportSort(list, sort, valueOf, nameOf) {
  const cmp = (a, b, key, dir) => {
    const va = valueOf(a, key), vb = valueOf(b, key);
    if (va == null || vb == null) return va == null && vb == null ? 0 : va == null ? 1 : -1;
    return (typeof va === 'string' ? va.localeCompare(vb, 'th') : va - vb) * dir;
  };
  return list.slice().sort((a, b) => cmp(a, b, sort.key, sort.dir) || cmp(a, b, 'R', -1) || nameOf(a).localeCompare(nameOf(b), 'th'));
}

/** ค่าที่ใช้เรียงของ "กลุ่ม" (ฝ่าย/รหัส/หมวด) และของ "บรรทัด" */
function reportGroupValue(g, k) {
  if (k === 'name') return g.code + g.name;
  if (k === 'status') return g.s.red * 1e6 + g.s.yellow;
  if (k === 'diff' || k === 'pct') return g.s.complete ? g.s[k] : null;
  if (k === 'share') return g.s.R;
  return g.s[k];
}
function reportLineValue(x, k) {
  const ev = x.ev;
  if (k === 'status') return FLAG_RANK[ev.flag];
  if (k === 'red' || k === 'yellow') return ev.flag === k ? 1 : 0;
  if (k === 'R' || k === 'share') return ev.filled ? ev.R : null;
  if (k === 'diff' || k === 'pct') return ev.filled ? ev[k] : null;
  return ev[k];
}

const signedAmount = (x) => (x > 0 ? '+' : '') + fmt(x);
/** ตัวเลขย่อสำหรับแกนกราฟ: 2.5M = 2.5 ล้าน, 850K = 850 พัน */
const fmtCompact = (x) => (Math.abs(x) >= 1e6 ? (x / 1e6).toFixed(1).replace(/\.0$/, '') + 'M'
  : Math.abs(x) >= 1e3 ? (x / 1e3).toFixed(1).replace(/\.0$/, '') + 'K' : String(Math.round(x)));

/**
 * โหมดนำเสนอ (ใช้ร่วมทุกหน้ารายงาน): ซ่อนเมนู และขอเต็มจอ — กด Esc หรือปุ่มเดิมเพื่อออก
 * เบราว์เซอร์ไม่ยอมให้เต็มจอก็ยังอยู่ในโหมดนำเสนอได้ · คืน [กำลังนำเสนออยู่ไหม, ฟังก์ชันสลับ]
 */
function usePresentMode() {
  const [present, setPresent] = useState(false);
  useEffect(() => {
    document.body.classList.toggle('present', present);
    return () => document.body.classList.remove('present');
  }, [present]);
  useEffect(() => {
    const onFs = () => { if (!document.fullscreenElement) setPresent(false); };
    document.addEventListener('fullscreenchange', onFs);
    return () => document.removeEventListener('fullscreenchange', onFs);
  }, []);
  const toggle = () => {
    if (present) {
      setPresent(false);
      if (document.fullscreenElement) document.exitFullscreen().catch(() => {});
      return;
    }
    setPresent(true);
    const el = document.documentElement;
    if (el.requestFullscreen) el.requestFullscreen().catch(() => {});
  };
  return [present, toggle];
}

/* ═══════════════════════ กราฟ ═══════════════════════ */

/**
 * เส้นรายเดือน ม.ค.–ธ.ค. ของชุดบรรทัด: Budget / Actual / Forecast (เดือนที่ยังไม่มี Actual = Actual เฉลี่ยต่อเดือน) / ยอดขออนุมัติ
 * cum = true ให้เป็นยอดสะสมตั้งแต่ ม.ค. · total ของแต่ละเส้นรับจากยอดรวม (agg) เพื่อให้ตรงกับตัวเลขในตาราง
 */
function reportSeries(items, cfg, L, cum, agg) {
  const n = Math.min(12, Math.max(0, Number(cfg.actualMonths) || 0));
  const B = R.zeros(), A = R.zeros(), Q = R.zeros();
  let anyQ = false;
  for (const { l } of items) {
    for (let i = 0; i < 12; i++) {
      B[i] += Number(l.budget[i]) || 0;
      if (i < n) A[i] += Number(l.actual[i]) || 0;
      if (Array.isArray(l.months)) { Q[i] += Number(l.months[i]) || 0; anyQ = true; }
    }
  }
  const run = (arr) => { let s = 0; return arr.map((v) => (s += v)); };
  const shape = (arr) => (cum ? run(arr) : arr).map(R.round2);
  const cumA = run(A);
  const avg = n > 0 ? cumA[n - 1] / n : 0;
  // เส้น Forecast ต่อจากจุด Actual เดือนล่าสุดไปถึง ธ.ค.
  const fc = B.map((_, i) => {
    if (!(n > 0 && n < 12) || i < n - 1) return null;
    if (cum) return R.round2(cumA[n - 1] + avg * (i - (n - 1)));
    return R.round2(i === n - 1 ? A[i] : avg);
  });
  const out = [
    { key: 'B', name: L.budget, color: CHART_COLORS.B, dash: '7 4', values: shape(B), total: agg.B },
    { key: 'A', name: L.actual, color: CHART_COLORS.A, values: shape(A).map((v, i) => (i < n ? v : null)), total: agg.A },
  ];
  if (n > 0 && n < 12) out.push({ key: 'F', name: L.forecast, color: CHART_COLORS.A, dash: '2 5', values: fc, total: agg.P });
  out.push({ key: 'R', name: L.request, color: CHART_COLORS.R, width: 3.2, values: anyQ ? shape(Q) : Q.map(() => null), total: anyQ ? agg.R : null });
  return out;
}

/** ความกว้างปัจจุบันของกล่อง (px) — ให้กราฟวาดเต็มกล่องโดยตัวหนังสือไม่ยืดหด */
function useElementWidth(ref) {
  const [w, setW] = useState(0);
  useEffect(() => {
    const el = ref.current;
    if (!el) return undefined;
    const update = () => setW(el.clientWidth);
    update();
    // ฟังทั้งสองทาง: ResizeObserver จับตอนกล่องเปลี่ยนขนาดเอง (เช่น เข้าโหมดนำเสนอ) · resize จับตอนย่อ/ขยายหน้าต่าง
    window.addEventListener('resize', update);
    const ro = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(update);
    if (ro) ro.observe(el);
    return () => { window.removeEventListener('resize', update); if (ro) ro.disconnect(); };
  }, [ref]);
  return w;
}

/** ขีดแกนตั้งแบบเลขกลม เริ่มที่ 0 และขีดบนสุดไม่ต่ำกว่าค่าสูงสุด */
function niceTicks(max, count) {
  if (!(max > 0)) return [0, 1];
  const raw = max / count;
  const pow = Math.pow(10, Math.floor(Math.log10(raw)));
  const step = [1, 2, 2.5, 5, 10].map((m) => m * pow).find((s) => s >= raw);
  const out = [];
  for (let i = 0; i <= count + 2; i++) { out.push(R.round2(step * i)); if (step * i >= max) break; }
  return out;
}

/** กราฟเส้น: series = [{ key, name, color, dash?, width?, values: (number|null)[], total }] · ชี้ที่เดือนเพื่อดูตัวเลขของเดือนนั้น */
function LineChart({ series, labels, height, totalLabel }) {
  const box = useRef(null);
  const width = useElementWidth(box);
  const [hi, setHi] = useState(null);
  const H = height || 260;
  const m = { l: 50, r: 16, t: 10, b: 26 };
  const max = Math.max(0, ...series.flatMap((s) => s.values.filter((v) => v != null)));
  const ticks = niceTicks(max, H < 200 ? 3 : 4);
  const top = ticks[ticks.length - 1] || 1;
  const n = labels.length;
  const iw = Math.max(10, width - m.l - m.r), ih = H - m.t - m.b;
  const step = n > 1 ? iw / (n - 1) : iw;
  const x = (i) => m.l + (n > 1 ? step * i : iw / 2);
  const y = (v) => m.t + ih - (ih * v) / top;
  const path = (vals) => {
    let d = '', pen = false;
    vals.forEach((v, i) => {
      if (v == null) { pen = false; return; }
      d += (pen ? 'L' : 'M') + x(i).toFixed(1) + ' ' + y(v).toFixed(1);
      pen = true;
    });
    return d;
  };
  return (
    <div className="chart" ref={box}>
      <div className="readout">
        <span className="when">{hi == null ? totalLabel : labels[hi]}</span>
        {series.map((s) => {
          const v = hi == null ? s.total : s.values[hi];
          return (
            <span key={s.key}>
              <svg className="sw" width="22" height="8" aria-hidden="true"><line x1="1" x2="21" y1="4" y2="4" stroke={s.color} strokeWidth="3" strokeDasharray={s.dash ? s.dash.split(' ').map((p) => Number(p) / 2).join(' ') : undefined} strokeLinecap="round" /></svg>
              {s.name} <b>{v == null ? '–' : fmt(v)}</b>
            </span>
          );
        })}
      </div>
      {width > 0 && (
        <svg width={width} height={H} role="img" aria-label={'กราฟเส้นเทียบ ' + series.map((s) => s.name).join(' / ')} onMouseLeave={() => setHi(null)}>
          {ticks.map((t) => (
            <g key={t}>
              <line x1={m.l} x2={width - m.r} y1={y(t)} y2={y(t)} className="grid" />
              <text x={m.l - 8} y={y(t)} className="tick" textAnchor="end" dominantBaseline="middle">{fmtCompact(t)}</text>
            </g>
          ))}
          {labels.map((lb, i) => <text key={lb} x={x(i)} y={H - 7} className={'tick' + (hi === i ? ' on' : '')} textAnchor="middle">{lb}</text>)}
          {hi != null && <line x1={x(hi)} x2={x(hi)} y1={m.t} y2={m.t + ih} className="guide" />}
          {series.map((s) => (
            <path key={s.key} d={path(s.values)} fill="none" stroke={s.color} strokeWidth={s.width || 2.4} strokeDasharray={s.dash} strokeLinejoin="round" strokeLinecap="round" />
          ))}
          {series.filter((s) => s.key !== 'F').map((s) => s.values.map((v, i) => (v == null ? null : (
            <circle key={s.key + i} cx={x(i)} cy={y(v)} r={hi === i ? 4.5 : 2.6} fill={hi === i ? s.color : '#fff'} stroke={s.color} strokeWidth="2" />
          ))))}
          {labels.map((lb, i) => (
            <rect key={'hit' + lb} x={x(i) - step / 2} y={m.t} width={step} height={ih + m.b} fill="transparent" onMouseEnter={() => setHi(i)} />
          ))}
        </svg>
      )}
    </div>
  );
}

/** แท่งเทียบ Budget กับยอดขออนุมัติ ของ 10 กลุ่มที่ขอมากที่สุด */
function TopBars({ groups, view, L }) {
  const top = groups.slice().sort((a, b) => b.s.R - a.s.R || b.s.B - a.s.B).slice(0, 10);
  const max = Math.max(1, ...top.map((g) => Math.max(g.s.B, g.s.R)));
  if (!top.length) return <div className="empty">ไม่มีข้อมูลตามตัวกรองนี้</div>;
  return (
    <div className="topbars">
      {top.map((g, i) => (
        <div className="row" key={g.key} title={`${g.code ? g.code + ' ' : ''}${g.name} · ${L.budget} ${fmt(g.s.B)} · ${L.request} ${g.s.filled ? fmt(g.s.R) : 'ยังไม่กรอก'}`}>
          <span className="rk">{i + 1}</span>
          <span className="nm">{view === 'dept' ? <><b>{g.code}</b> {g.name}</> : g.name}</span>
          <span className="bars big">
            <i className="b" style={{ width: (g.s.B / max) * 100 + '%' }} /><i className="r" style={{ width: (g.s.R / max) * 100 + '%' }} />
          </span>
          <span className="num val">{g.s.filled ? fmt(g.s.R) : '–'}</span>
        </div>
      ))}
    </div>
  );
}

/* ═══════════════════════ Excel ═══════════════════════ */

function exportReport(cfg, view, label, groups, rows, total) {
  const L = labelsOf(cfg);
  const pct1 = (p) => (p == null || !isFinite(p) ? null : Math.round(p * 10) / 10);
  const head = view === 'dept' ? ['รหัสฝ่าย', 'ชื่อฝ่าย'] : view === 'acct' ? ['รหัสบัญชี', 'ชื่อค่าใช้จ่าย', 'หมวด'] : ['หมวดค่าใช้จ่าย'];
  const summary = [[...head, 'จำนวนรายการ', 'กรอกแล้ว', L.budget, L.actual, L.forecast, L.request, 'ส่วนต่าง', L.pct, 'สัดส่วนของยอดขอ (%)', FLAG_LABEL.red, FLAG_LABEL.yellow]];
  for (const g of groups) {
    const s = g.s;
    const left = view === 'dept' ? [g.code, g.name] : view === 'acct' ? [g.code, g.name, g.sub] : [g.name];
    summary.push([...left, s.n, s.filled, s.B, s.A, s.P, s.R, s.complete ? s.diff : null, s.complete ? pct1(s.pct) : null,
      total.R > 0 ? pct1((s.R / total.R) * 100) : null, s.red, s.yellow]);
  }
  const detail = [['รหัสฝ่าย', 'ชื่อฝ่าย', 'รหัสบัญชี', 'ชื่อค่าใช้จ่าย', 'หมวด', L.budget, L.actual, L.forecast, L.request, 'ส่วนต่าง', L.pct, 'สถานะ', 'คำชี้แจง', 'ยืนยัน']];
  for (const { d, l, ev } of rows) {
    detail.push([d.code, d.name, l.acct, l.name, l.cat, ev.B, ev.A, ev.P, ev.filled ? ev.R : null, ev.filled ? ev.diff : null,
      ev.filled ? pct1(ev.pct) : null, FLAG_LABEL[ev.flag], l.note, l.confirmed ? 'ยืนยันแล้ว' : '']);
  }
  const wb = XLSX.utils.book_new();
  const add = (name, aoa, widths) => {
    const ws = XLSX.utils.aoa_to_sheet(aoa);
    ws['!cols'] = widths.map((wch) => ({ wch }));
    XLSX.utils.book_append_sheet(wb, ws, name);
  };
  add('สรุป', summary, [...head.map((_, i) => (i === 0 && head.length > 1 ? 10 : 36)), 12, 10, 16, 16, 18, 16, 16, 10, 12, 10, 10]);
  add('รายการ', detail, [10, 30, 10, 40, 26, 14, 14, 18, 16, 14, 10, 11, 60, 11]);
  const d = new Date();
  const stamp = d.getFullYear() + String(d.getMonth() + 1).padStart(2, '0') + String(d.getDate()).padStart(2, '0');
  XLSX.writeFile(wb, `รายงานงบประมาณ${cfg.year}_${label}_${stamp}.xlsx`);
}

/* ═══════════════════════ ตัวเลือกค่าใช้จ่าย ═══════════════════════ */

/**
 * ช่องค้นค่าใช้จ่ายพร้อมรายการให้ติ๊กเลือก (เลือกได้หลายรายการ)
 * ติ๊กแล้วกล่องสรุป กราฟ และตารางทั้งหน้าจะเป็นข้อมูลของรายการที่เลือก · ยังไม่ติ๊ก = ใช้คำค้นกรองแทน
 * options = [{ code, name, cat, R }] (R = ยอดขออนุมัติรวมของรหัสนั้น)
 */
function ExpensePicker({ options, selected, onChange, text, onText }) {
  const [open, setOpen] = useState(false);
  const box = useRef(null);
  const hasSel = selected.length > 0;
  // ปิดรายการ: ถ้าติ๊กเลือกไว้แล้ว ล้างคำค้นทิ้ง (ตัวกรองคือรายการที่ติ๊ก ไม่ใช่คำค้น)
  const close = useCallback(() => { setOpen(false); if (hasSel) onText(''); }, [hasSel, onText]);
  useEffect(() => {
    if (!open) return undefined;
    const onDown = (e) => { if (box.current && !box.current.contains(e.target)) close(); };
    const onKey = (e) => { if (e.key === 'Escape') close(); };
    document.addEventListener('mousedown', onDown);
    document.addEventListener('keydown', onKey);
    return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey); };
  }, [open, close]);
  const needle = text.trim().toLowerCase();
  const shown = options.filter((o) => !needle || o.code.includes(needle) || o.name.toLowerCase().includes(needle) || o.cat.toLowerCase().includes(needle));
  const set = new Set(selected);
  const toggle = (code) => onChange(set.has(code) ? selected.filter((c) => c !== code) : [...selected, code]);
  const allShownOn = shown.length > 0 && shown.every((o) => set.has(o.code));
  const toggleShown = () => onChange(allShownOn
    ? selected.filter((c) => !shown.some((o) => o.code === c))
    : Array.from(new Set([...selected, ...shown.map((o) => o.code)])));
  return (
    <div className="picker" ref={box}>
      <div className="search">
        <Icon name="search" />
        <input
          type="text" value={text} aria-label="ค้นและติ๊กเลือกค่าใช้จ่าย" aria-expanded={open} style={{ width: 310 }}
          placeholder={hasSel ? `เลือกแล้ว ${selected.length} รายการ · พิมพ์เพื่อเลือกเพิ่ม` : 'ค้นแล้วติ๊กเลือกค่าใช้จ่าย (ชื่อ รหัส หรือหมวด)'}
          onFocus={() => setOpen(true)} onChange={(e) => { onText(e.target.value); setOpen(true); }}
        />
      </div>
      {open && (
        <div className="picker-panel">
          <div className="picker-head">
            <span>{needle ? `พบ ${shown.length} รายการ` : `ทั้งหมด ${shown.length} รายการ`}{hasSel && ` · เลือกแล้ว ${selected.length}`}</span>
            <span style={{ flex: 1 }} />
            {needle && shown.length > 0 && <button className="link" onClick={toggleShown}>{allShownOn ? 'เอาที่พบออกทั้งหมด' : 'เลือกทั้งหมดที่พบ'}</button>}
            {hasSel && <button className="link" onClick={() => onChange([])}>ล้างที่เลือก</button>}
          </div>
          <div className="picker-list" role="group" aria-label="รายการค่าใช้จ่ายให้ติ๊กเลือก">
            {shown.length === 0 && <div className="empty">ไม่พบค่าใช้จ่ายที่ตรงกับคำค้น</div>}
            {shown.map((o) => (
              <label key={o.code} className={set.has(o.code) ? 'on' : ''}>
                <input type="checkbox" checked={set.has(o.code)} onChange={() => toggle(o.code)} />
                <span className="code">{o.code}</span>
                <span className="nm">{o.name}</span>
                <span className="cat">{o.cat}</span>
                <span className="num req">{o.R > 0 ? fmt(o.R) : ''}</span>
              </label>
            ))}
          </div>
          <div className="picker-foot">
            <span className="mute small">ติ๊กได้หลายรายการ · ตัวเลขและกราฟทั้งหน้าจะเปลี่ยนตามที่เลือก</span>
            <span style={{ flex: 1 }} />
            <button className="btn sm primary" onClick={close}>เสร็จ</button>
          </div>
        </div>
      )}
    </div>
  );
}

/* ═══════════════════════ หน้ารายงาน ═══════════════════════ */

function ReportPage({ go }) {
  const { all, err, load } = useAdminAll();
  const [view, setView] = useState('dept');
  const [dept, setDept] = useState('');
  const [q, setQ] = useState('');                   // คำค้นค่าใช้จ่าย (ใช้กรองเมื่อยังไม่ได้ติ๊กเลือก)
  const [sel, setSel] = useState([]);               // รหัสบัญชีที่ติ๊กเลือก — มีแล้วทั้งหน้าเป็นข้อมูลของรหัสเหล่านี้
  const [flag, setFlag] = useState(null);           // null | 'red' | 'yellow'
  const [sentOnly, setSentOnly] = useState(false);  // นับเฉพาะฝ่ายที่ส่งงบแล้ว (ส่งแล้ว/ล็อกแล้ว)
  const [sort, setSort] = useState({ key: 'R', dir: -1 });
  const [open, setOpen] = useState({});
  const [cum, setCum] = useState(false);            // กราฟเส้น: รายเดือน หรือ สะสมตั้งแต่ ม.ค.
  const [present, togglePresent] = usePresentMode();
  const flat = useMemo(() => {
    const out = [];
    if (all) for (const d of all.depts) for (const l of d.lines) out.push({ d, l, ev: R.evaluateLine(l, all.round) });
    return out;
  }, [all]);
  const acctName = useMemo(() => {
    const m = new Map();
    for (const x of flat) if (!m.has(x.l.acct)) m.set(x.l.acct, x.l.name);
    return m;
  }, [flat]);

  if (err) return <div className="card pad"><p className="err">{err}</p><p><button className="btn sm" onClick={load}>ลองใหม่</button></p></div>;
  if (!all) return <div className="boot">กำลังโหลด…</div>;
  const cfg = all.round;
  const L = labelsOf(cfg);
  const isSent = (d) => d.status === 'submitted' || d.status === 'locked';

  // ── ตัวกรองชุดเดียว ใช้กับกล่องสรุป กราฟ และตารางทั้งหน้า ──
  //   ขอบเขตฝ่าย (ทุกฝ่าย/เฉพาะที่ส่งแล้ว + ฝ่ายที่เลือก) → ค่าใช้จ่าย (ที่ติ๊กเลือก หรือคำค้นถ้ายังไม่ติ๊ก) → สถานะ
  const scopeDepts = all.depts.filter((d) => !sentOnly || isSent(d));
  const scoped = flat.filter((x) => !sentOnly || isSent(x.d));
  const inDept = scoped.filter((x) => !dept || x.d.code === dept);
  const needle = q.trim().toLowerCase();
  const picked = sel.length > 0;
  const selSet = new Set(sel);
  const base = inDept.filter((x) => (picked ? selSet.has(x.l.acct)
    : !needle || x.l.acct.includes(needle) || x.l.name.toLowerCase().includes(needle) || (x.l.cat || '').toLowerCase().includes(needle)));
  const rows = base.filter((x) => !flag || x.ev.flag === flag);
  const T = aggregateLines(rows);
  const filtered = !!dept || picked || !!needle || !!flag;
  const clearFilters = () => { setDept(''); setSel([]); setQ(''); setFlag(null); };
  // ฝ่ายที่เกี่ยวข้องกับข้อมูลที่กำลังดู (ไม่กรอง = ทุกฝ่าย)
  const rowDepts = new Set(rows.map((x) => x.d.code));
  const viewDepts = filtered ? all.depts.filter((d) => rowDepts.has(d.code)) : all.depts;
  const nSent = viewDepts.filter(isSent).length;
  // รายการค่าใช้จ่ายให้ติ๊กเลือก: ทุกรหัสที่ฝ่ายในขอบเขตมี พร้อมยอดขออนุมัติรวม
  const optMap = new Map();
  for (const x of inDept) {
    let o = optMap.get(x.l.acct);
    if (!o) { o = { code: x.l.acct, name: x.l.name, cat: x.l.cat || '', R: 0 }; optMap.set(o.code, o); }
    o.R += x.ev.R;
  }
  const options = Array.from(optMap.values()).sort((a, b) => (a.code < b.code ? -1 : 1));

  const spec = {
    dept: { key: (x) => x.d.code, code: (x) => x.d.code, name: (x) => x.d.name, sub: (x) => x.d.status || 'draft' },
    acct: { key: (x) => x.l.acct, code: (x) => x.l.acct, name: (x) => x.l.name, sub: (x) => x.l.cat || '' },
    cat: { key: (x) => x.l.cat || NO_CAT, code: () => '', name: (x) => x.l.cat || NO_CAT, sub: () => '' },
  };
  const groupBy = (items, sp) => {
    const map = new Map();
    for (const x of items) {
      const k = sp.key(x);
      let g = map.get(k);
      if (!g) { g = { key: k, code: sp.code(x), name: sp.name(x), sub: sp.sub(x), items: [] }; map.set(k, g); }
      g.items.push(x);
    }
    const list = Array.from(map.values());
    list.forEach((g) => { g.s = aggregateLines(g.items); });
    return reportSort(list, sort, reportGroupValue, (g) => g.code + g.name);
  };
  // มุมมอง "คำชี้แจง" เป็นรายการเรียงต่อกัน ไม่จัดกลุ่ม — กราฟ 10 อันดับและ Excel ของมุมมองนั้นสรุปตามฝ่าย
  const gview = view === 'note' ? 'dept' : view;
  const groups = groupBy(rows, spec[gview]);
  const maxBar = Math.max(1, ...groups.map((g) => Math.max(g.s.B, g.s.R)));
  const isOpen = (g) => (open[view + '/' + g.key] != null ? open[view + '/' + g.key] : groups.length === 1);
  const toggle = (g) => setOpen((o) => ({ ...o, [view + '/' + g.key]: !isOpen(g) }));
  const allOpen = groups.length > 0 && groups.every(isOpen);
  const setAll = (on) => setOpen((o) => ({ ...o, ...Object.fromEntries(groups.map((g) => [view + '/' + g.key, on])) }));
  // บรรทัดที่มีคำชี้แจง หรือเป็นรายการต้องชี้แจงที่ยังไม่ได้ใส่
  const noteRows = view !== 'note' ? [] : reportSort(
    rows.filter((x) => String(x.l.note || '').trim() || x.ev.flag === 'red'), sort,
    (x, k) => (k === 'name' ? x.d.code + x.l.acct : reportLineValue(x, k)), (x) => x.d.code + x.l.acct,
  );
  const N = aggregateLines(noteRows);

  // ── หัวตารางที่คลิกเรียงได้ ──
  const sortKey = sort.key + ':' + sort.dir;
  const sorts = [
    { v: 'R:-1', label: 'ยอดขออนุมัติ มาก → น้อย' },
    { v: 'R:1', label: 'ยอดขออนุมัติ น้อย → มาก' },
    { v: 'diff:-1', label: 'ส่วนต่างจาก Budget มาก → น้อย' },
    { v: 'pct:-1', label: L.pct + ' มาก → น้อย' },
    { v: 'status:-1', label: FLAG_LABEL.red + ' / ' + FLAG_LABEL.yellow + ' ขึ้นก่อน' },
    { v: 'B:-1', label: L.budget + ' มาก → น้อย' },
    { v: 'name:1', label: 'รหัส / ชื่อ' },
  ];
  const sortLabel = (sorts.find((s) => s.v === sortKey) || { label: 'ตามหัวตารางที่คลิก' }).label;
  const th = (k, label, num) => (
    <th
      key={k} className={(num ? 'num ' : '') + 'sortable' + (sort.key === k ? ' on' : '')} title="คลิกเพื่อเรียง"
      aria-sort={sort.key === k ? (sort.dir < 0 ? 'descending' : 'ascending') : 'none'}
      onClick={(e) => { e.stopPropagation(); setSort((s) => ({ key: k, dir: s.key === k ? -s.dir : k === 'name' ? 1 : -1 })); }}
    >
      {label}{sort.key === k && <span className="arrow">{sort.dir < 0 ? '▼' : '▲'}</span>}
    </th>
  );

  const viewInfo = REPORT_VIEWS.find((v) => v.key === view);
  const deptName = dept ? (all.depts.find((d) => d.code === dept) || { name: dept }).name : '';
  const expenseText = picked
    ? (sel.length <= 2 ? sel.map((c) => acctName.get(c) || c).join(', ') : 'ค่าใช้จ่ายที่เลือก ' + sel.length + ' รายการ')
    : needle ? 'ค่าใช้จ่ายที่มีคำว่า "' + q.trim() + '"' : null;
  const scopeText = [
    expenseText,
    dept ? 'ฝ่าย ' + dept + ' ' + deptName : sentOnly ? 'เฉพาะฝ่ายที่ส่งงบแล้ว' : 'ทุกฝ่าย',
    flag ? 'เฉพาะรายการ' + FLAG_LABEL[flag] : null,
  ].filter(Boolean).join(' · ');
  const describe = viewInfo.label + ' · ' + scopeText + ' · เรียง: ' + sortLabel;

  const diffCell = (s) => (s.complete ? signedAmount(s.diff) : <span className="mute small">กรอก {s.filled}/{s.n}</span>);
  const pctCell = (s) => (!s.complete ? '' : s.B > 0 ? fmtPct(s.pct) : s.R > 0 ? 'ใหม่' : '–');
  const statusTag = (ev) => (ev.flag === 'none' ? <span className="mute small">–</span> : <span className={'tag ' + ev.flag} title={R.flagText(ev, cfg)}>{FLAG_LABEL[ev.flag]}</span>);
  const noteCell = (l, ev) => l.note || (ev.flag === 'red' ? <span className="err">ยังไม่ใส่คำชี้แจง</span> : <span className="mute small">–</span>);
  const linePct = (ev) => (!ev.filled ? '' : ev.pct == null ? (ev.R > 0 ? 'ใหม่' : '–') : fmtPct(ev.pct));
  const totalLabel = cum ? 'สะสมถึงสิ้นปี' : 'รวมทั้งปี';

  /** รายการข้างในของกลุ่ม: ฝ่าย → ค่าใช้จ่ายของฝ่ายนั้น · ค่าใช้จ่าย → ฝ่ายที่ขอ · หมวด → ค่าใช้จ่ายในหมวด (รวมทุกฝ่าย) */
  const renderDetail = (g) => {
    const mini = (
      <div className="minichart">
        <LineChart series={reportSeries(g.items, cfg, L, cum, g.s)} labels={R.MONTHS_TH} height={170} totalLabel={totalLabel} />
      </div>
    );
    if (view === 'cat') {
      const subs = groupBy(g.items, spec.acct);
      return (
        <>
          {mini}
          <table className="t inner">
            <thead>
              <tr>
                {th('name', 'ค่าใช้จ่าย (รหัสบัญชี)')}<th className="num">จำนวนฝ่าย</th>{th('B', L.budget, true)}{th('A', L.actual, true)}{th('P', L.forecast, true)}
                {th('R', L.request, true)}{th('diff', 'ส่วนต่าง', true)}{th('pct', L.pct, true)}{th('red', FLAG_LABEL.red, true)}{th('yellow', FLAG_LABEL.yellow, true)}
              </tr>
            </thead>
            <tbody>
              {subs.map((a) => (
                <tr key={a.key}>
                  <td><span className="mute small">{a.code}</span> {a.name}</td>
                  <td className="num">{a.s.n}</td>
                  <td className="num">{fmt(a.s.B)}</td><td className="num">{fmt(a.s.A)}</td><td className="num">{fmt(a.s.P)}</td>
                  <td className="num"><b>{a.s.filled ? fmt(a.s.R) : '–'}</b></td>
                  <td className="num">{diffCell(a.s)}</td><td className="num">{pctCell(a.s)}</td>
                  <td className={'num' + (a.s.red ? ' red' : '')}>{a.s.red || ''}</td>
                  <td className={'num' + (a.s.yellow ? ' yellow' : '')}>{a.s.yellow || ''}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </>
      );
    }
    const byDept = view === 'dept';
    const lines = reportSort(g.items, sort, (x, k) => (k === 'name' ? (byDept ? x.l.acct : x.d.code) : reportLineValue(x, k)), (x) => (byDept ? x.l.acct : x.d.code));
    return (
      <>
        {mini}
        <table className="t inner">
          <thead>
            <tr>
              {th('name', byDept ? 'ค่าใช้จ่าย (รหัสบัญชี)' : 'ฝ่าย')}{byDept && <th>หมวด</th>}{th('status', 'สถานะ')}
              {th('B', L.budget, true)}{th('A', L.actual, true)}{th('P', L.forecast, true)}{th('R', L.request, true)}
              {th('diff', 'ส่วนต่าง', true)}{th('pct', L.pct, true)}<th>คำชี้แจง</th>
            </tr>
          </thead>
          <tbody>
            {lines.map(({ d, l, ev }) => (
              <tr key={d.code + '/' + l.acct}>
                {byDept
                  ? <td><span className="mute small">{l.acct}</span> {l.name}{l.added && <> <span className="badge">เพิ่มใหม่</span></>}</td>
                  : <td><b>{d.code}</b> <span className="mute small">{d.name}</span>{l.added && <> <span className="badge">เพิ่มใหม่</span></>}</td>}
                {byDept && <td className="mute small">{l.cat}</td>}
                <td>{statusTag(ev)}</td>
                <td className="num">{fmt(ev.B)}</td>
                <td className="num">{fmt(ev.A)}</td>
                <td className="num">{ev.P == null ? '–' : fmt(ev.P)}</td>
                <td className="num">{ev.filled ? <b>{fmt(ev.R)}</b> : <span className="mute small">ยังไม่กรอก</span>}</td>
                <td className={'num ' + ev.flag}>{ev.filled ? signedAmount(ev.diff) : ''}</td>
                <td className={'num ' + ev.flag}>{linePct(ev)}</td>
                <td className="notecell">{noteCell(l, ev)}</td>
              </tr>
            ))}
          </tbody>
        </table>
        {byDept && (
          <div className="subact noprint"><button className="btn sm" onClick={() => go('dept/' + encodeURIComponent(g.key))}>เปิดหน้ากรอกงบของ {g.key}</button></div>
        )}
      </>
    );
  };

  const firstCol = view === 'dept' ? 'ฝ่าย' : view === 'acct' ? 'ค่าใช้จ่าย (รหัสบัญชี)' : 'หมวดค่าใช้จ่าย';
  const forecastGap = T.complete && T.P > 0 ? ((T.R - T.P) / T.P) * 100 : null;
  const noData = scoped.length === 0 ? 'ยังไม่มีฝ่ายที่ส่งงบ' : 'ไม่พบรายการตามตัวกรองนี้';
  const topTitle = gview === 'dept' ? 'ฝ่าย' : gview === 'acct' ? 'ค่าใช้จ่าย' : 'หมวดค่าใช้จ่าย';
  return (
    <div className="report">
      <div className="head">
        <div className="grow">
          <h1>รายงานงบประมาณปี {cfg.year}</h1>
          <div className="mute small">{describe}</div>
        </div>
        <button className="btn noprint" onClick={togglePresent}><Icon name={present ? 'x' : 'screen'} />{present ? 'ออกจากโหมดนำเสนอ' : 'โหมดนำเสนอ'}</button>
        <button className="btn noprint" onClick={() => window.print()}><Icon name="print" />พิมพ์ / PDF</button>
        <button className="btn noprint" onClick={() => exportReport(cfg, gview, viewInfo.label, groups, view === 'note' ? noteRows : rows, T)}><Icon name="download" />Export Excel</button>
      </div>

      {/* ── ตัวกรอง: มีผลกับกล่องสรุป กราฟ และตารางด้านล่างทั้งหมด ── */}
      <div className="card rfilters noprint">
        <div className="toolbar">
          <div className="seg lg" role="group" aria-label="มุมมองของรายงาน">
            {REPORT_VIEWS.map((v) => <button key={v.key} className={view === v.key ? 'on' : ''} onClick={() => setView(v.key)}>{v.label}</button>)}
          </div>
          <span className="sep" />
          <select value={dept} onChange={(e) => setDept(e.target.value)} aria-label="เลือกฝ่าย" style={{ height: 30, fontSize: 13, maxWidth: 260 }}>
            <option value="">ทุกฝ่าย</option>
            {scopeDepts.map((d) => <option key={d.code} value={d.code}>{d.code} · {d.name}</option>)}
          </select>
          <ExpensePicker options={options} selected={sel} onChange={setSel} text={q} onText={setQ} />
          <div className="chips" role="group" aria-label="กรองตามสถานะ">
            <button className={'chip red' + (flag === 'red' ? ' on' : '')} aria-pressed={flag === 'red'} onClick={() => setFlag(flag === 'red' ? null : 'red')}>{FLAG_LABEL.red} {base.filter((x) => x.ev.flag === 'red').length}</button>
            <button className={'chip yellow' + (flag === 'yellow' ? ' on' : '')} aria-pressed={flag === 'yellow'} onClick={() => setFlag(flag === 'yellow' ? null : 'yellow')}>{FLAG_LABEL.yellow} {base.filter((x) => x.ev.flag === 'yellow').length}</button>
            <button className={'chip done' + (sentOnly ? ' on' : '')} aria-pressed={sentOnly} onClick={() => { setSentOnly(!sentOnly); setDept(''); }} title="นับเฉพาะฝ่ายที่กดส่งงบแล้ว (รวมที่ล็อกแล้ว)">เฉพาะฝ่ายที่ส่งแล้ว</button>
          </div>
          <div style={{ flex: 1 }} />
          <select
            value={sorts.some((s) => s.v === sortKey) ? sortKey : ''} aria-label="เรียงตาม" style={{ height: 30, fontSize: 13 }}
            onChange={(e) => { if (e.target.value) { const [key, dir] = e.target.value.split(':'); setSort({ key, dir: Number(dir) }); } }}
          >
            {!sorts.some((s) => s.v === sortKey) && <option value="">เรียง: ตามหัวตารางที่คลิก</option>}
            {sorts.map((s) => <option key={s.v} value={s.v}>เรียง: {s.label}</option>)}
          </select>
          {view !== 'note' && groups.length > 1 && <button className="btn sm" onClick={() => setAll(!allOpen)}>{allOpen ? 'พับทั้งหมด' : 'ขยายทั้งหมด'}</button>}
        </div>
        {picked && (
          <div className="selchips">
            <span className="mute small">ค่าใช้จ่ายที่เลือก:</span>
            {sel.map((c) => (
              <span className="selchip" key={c}>
                <span className="code">{c}</span>{acctName.get(c) || c}
                <button onClick={() => setSel(sel.filter((x) => x !== c))} aria-label={'เอา ' + (acctName.get(c) || c) + ' ออก'} title="เอาออก"><Icon name="x" /></button>
              </span>
            ))}
            <button className="link" onClick={() => setSel([])}>ล้างที่เลือก</button>
          </div>
        )}
      </div>

      <div className="scopebar">
        <span className="mute">ตัวเลขและกราฟด้านล่างเป็นของ</span> <b>{scopeText}</b>
        {filtered && <button className="link noprint" onClick={clearFilters}>ล้างตัวกรองทั้งหมด</button>}
      </div>

      <div className="tiles">
        <div className="tile t-request big">
          <div className="k">{L.request}{filtered ? '' : sentOnly ? ' รวม (ฝ่ายที่ส่งแล้ว)' : ' รวมทุกฝ่าย'}</div>
          <div className="v">{fmt(T.R)} <span className="s">บาท</span></div>
          <div className="s">
            {T.n === 0 ? noData
              : T.complete ? `${signedAmount(T.diff)} บาท${T.pct != null ? ' (' + fmtPct(T.pct) + ')' : ''} เทียบ ${L.budget}`
              : `กรอกแล้ว ${T.filled}/${T.n} รายการ · ส่วนต่างจะแสดงเมื่อกรอกครบ`}
          </div>
        </div>
        <div className="tile t-budget"><div className="k">{L.budget}{filtered ? '' : ' รวม'}</div><div className="v">{fmt(T.B)}</div><div className="s">{L.actual} {fmt(T.A)}</div></div>
        <div className="tile t-forecast">
          <div className="k">{L.forecast}</div><div className="v">{fmt(T.P)}</div>
          <div className="s">{forecastGap != null ? `ยอดขอปี ${cfg.year} ${fmtPct(forecastGap)} เทียบ Forecast` : 'Actual เฉลี่ยต่อเดือน × 12'}</div>
        </div>
        <div className="tile t-status">
          <div className="k">ความคืบหน้า</div>
          <div className="v">{nSent}/{viewDepts.length} <span className="s">ฝ่ายส่งแล้ว</span></div>
          <div className="s"><Progress done={T.confirmed} total={T.n} label="ยืนยันแล้ว" /></div>
        </div>
        <div className="tile t-flag">
          <div className="k">รายการที่ต้องดู</div>
          <div className="v">
            <span className="nowrap">{T.red} <span className="s">{FLAG_LABEL.red}</span></span> · <span className="nowrap">{T.yellow} <span className="s">{FLAG_LABEL.yellow}</span></span>
          </div>
          <div className="s">{T.red ? `ขอเกิน Budget รวม ${fmt(T.over)} บาท${T.noteMissing ? ' · ยังไม่ใส่คำชี้แจง ' + T.noteMissing : ''}` : 'ยังไม่มีรายการที่ต้องชี้แจง'}</div>
        </div>
      </div>

      <div className="charts">
        <div className="card chartcard">
          <div className="charthead">
            <h2>เทียบรายเดือน{cum ? ' (ยอดสะสม)' : ''} <span className="mute small">· {scopeText}</span></h2>
            <div className="seg noprint" role="group" aria-label="รูปแบบกราฟเส้น">
              <button className={!cum ? 'on' : ''} onClick={() => setCum(false)}>รายเดือน</button>
              <button className={cum ? 'on' : ''} onClick={() => setCum(true)}>สะสม</button>
            </div>
          </div>
          {rows.length === 0 ? <div className="empty">ไม่มีข้อมูลตามตัวกรองนี้</div>
            : <LineChart series={reportSeries(rows, cfg, L, cum, T)} labels={R.MONTHS_TH} height={270} totalLabel={totalLabel} />}
          <div className="mute small chartnote">
            หน่วย: บาท (K = พัน, M = ล้าน) · เส้นประสั้น = Forecast ของเดือนที่ยังไม่มี Actual (ใช้ Actual เฉลี่ยต่อเดือน)
            {T.n > 0 && T.filled < T.n && ` · ยอดขออนุมัตินับเฉพาะรายการที่กรอกแล้ว ${T.filled}/${T.n}`}
          </div>
        </div>
        <div className="card chartcard">
          <div className="charthead">
            <h2>10 อันดับยอดขออนุมัติสูงสุด <span className="mute small">· {topTitle}</span></h2>
            <span className="legend dark"><span><i className="b" />{L.budget}</span><span><i className="r" />{L.request}</span></span>
          </div>
          <TopBars groups={groups} view={gview} L={L} />
        </div>
      </div>

      <div className="card tw">
        {view === 'note' ? (
            <table className="t">
              <thead>
                <tr>
                  {th('name', 'ฝ่าย')}<th>ค่าใช้จ่าย (รหัสบัญชี)</th>{th('status', 'สถานะ')}{th('B', L.budget, true)}{th('P', L.forecast, true)}
                  {th('R', L.request, true)}{th('diff', 'ส่วนต่าง', true)}{th('pct', L.pct, true)}<th>คำชี้แจงของฝ่าย</th>
                </tr>
              </thead>
              <tbody>
                {noteRows.length === 0 && <tr><td colSpan={9} className="empty">{rows.length === 0 ? 'ไม่พบรายการตามตัวกรองนี้' : 'ยังไม่มีคำชี้แจงในกลุ่มที่เลือก'}</td></tr>}
                {noteRows.map(({ d, l, ev }) => (
                  <tr key={d.code + '/' + l.acct}>
                    <td className="nowrap"><b>{d.code}</b></td>
                    <td><span className="mute small">{l.acct}</span> {l.name}{l.added && <> <span className="badge">เพิ่มใหม่</span></>}</td>
                    <td>{statusTag(ev)}</td>
                    <td className="num">{fmt(ev.B)}</td>
                    <td className="num">{ev.P == null ? '–' : fmt(ev.P)}</td>
                    <td className="num">{ev.filled ? <b>{fmt(ev.R)}</b> : <span className="mute small">ยังไม่กรอก</span>}</td>
                    <td className={'num ' + ev.flag}>{ev.filled ? signedAmount(ev.diff) : ''}</td>
                    <td className={'num ' + ev.flag}>{linePct(ev)}</td>
                    <td className="notecell wide">{noteCell(l, ev)}</td>
                  </tr>
                ))}
              </tbody>
              {noteRows.length > 0 && (
                <tfoot>
                  <tr>
                    <td colSpan={3}>รวม {noteRows.length} รายการ{N.noteMissing ? ` · ยังไม่ใส่คำชี้แจง ${N.noteMissing}` : ''}</td>
                    <td className="num">{fmt(N.B)}</td><td className="num">{fmt(N.P)}</td><td className="num">{fmt(N.R)}</td>
                    <td className="num">{diffCell(N)}</td><td className="num">{pctCell(N)}</td><td />
                  </tr>
                </tfoot>
              )}
            </table>
        ) : (
            <table className="t">
              <thead>
                <tr>
                  {th('name', firstCol)}{th('B', L.budget, true)}{th('A', L.actual, true)}{th('P', L.forecast, true)}{th('R', L.request, true)}
                  <th><span className="legend"><span><i className="b" />Budget</span><span><i className="r" />ยอดขอ</span></span></th>
                  {th('diff', 'ส่วนต่าง', true)}{th('pct', L.pct, true)}{th('share', 'สัดส่วน', true)}{th('red', FLAG_LABEL.red, true)}{th('yellow', FLAG_LABEL.yellow, true)}<th />
                </tr>
              </thead>
              <tbody>
                {groups.length === 0 && <tr><td colSpan={12} className="empty">{noData}</td></tr>}
                {groups.map((g) => {
                  const s = g.s, on = isOpen(g);
                  return (
                    <React.Fragment key={g.key}>
                      <tr className={'click grp' + (on ? ' open' : '')} onClick={() => toggle(g)} title="คลิกเพื่อดูกราฟและรายการข้างใน">
                        <td>
                          {view === 'dept' && <><b>{g.code}</b> <span className="mute small">{g.name}</span>{!isSent({ status: g.sub }) && <span className="mute small"> · {(STATUS[g.sub] || STATUS.draft).label}</span>}</>}
                          {view === 'acct' && <><span className="mute small">{g.code}</span> <b>{g.name}</b>{g.sub && <span className="mute small"> · {g.sub}</span>}</>}
                          {view === 'cat' && <><b>{g.name}</b> <span className="mute small">· {new Set(g.items.map((x) => x.l.acct)).size} รหัส</span></>}
                        </td>
                        <td className="num">{fmt(s.B)}</td>
                        <td className="num">{fmt(s.A)}</td>
                        <td className="num">{fmt(s.P)}</td>
                        <td className="num"><b>{s.filled ? fmt(s.R) : '–'}</b></td>
                        <td>
                          <span className="bars" title={`${L.budget} ${fmt(s.B)} · ${L.request} ${fmt(s.R)}`}>
                            <i className="b" style={{ width: (s.B / maxBar) * 100 + '%' }} /><i className="r" style={{ width: (s.R / maxBar) * 100 + '%' }} />
                          </span>
                        </td>
                        <td className="num">{diffCell(s)}</td>
                        <td className="num">{pctCell(s)}</td>
                        <td className="num">{T.R > 0 && s.R > 0 ? ((s.R / T.R) * 100).toFixed(1) + '%' : ''}</td>
                        <td className={'num' + (s.red ? ' red' : '')}>{s.red || ''}</td>
                        <td className={'num' + (s.yellow ? ' yellow' : '')}>{s.yellow || ''}</td>
                        <td className="ctr noprint"><Icon name="chevron" className={'chev' + (on ? ' open' : '')} /></td>
                      </tr>
                      {on && <tr className="sub"><td colSpan={12}>{renderDetail(g)}</td></tr>}
                    </React.Fragment>
                  );
                })}
              </tbody>
              {groups.length > 0 && (
                <tfoot>
                  <tr>
                    <td>รวม {groups.length} {viewInfo.unit}</td>
                    <td className="num">{fmt(T.B)}</td><td className="num">{fmt(T.A)}</td><td className="num">{fmt(T.P)}</td><td className="num">{fmt(T.R)}</td><td />
                    <td className="num">{diffCell(T)}</td><td className="num">{pctCell(T)}</td><td className="num">{T.R > 0 ? '100%' : ''}</td>
                    <td className="num">{T.red || ''}</td><td className="num">{T.yellow || ''}</td><td />
                  </tr>
                </tfoot>
              )}
            </table>
        )}
      </div>
      <p className="mute small" style={{ marginTop: 10 }}>
        กล่องสรุป กราฟ และตาราง ใช้ตัวกรองชุดเดียวกัน (ฝ่าย ค่าใช้จ่ายที่ติ๊กเลือก สถานะ) · "ส่วนต่าง" และ "{L.pct}" แสดงเมื่อกรอกครบทุกรายการในกลุ่มนั้นแล้ว ·
        คลิกหัวตารางเพื่อเรียง{view !== 'note' && ' คลิกแถวเพื่อดูกราฟและรายการข้างใน'}
      </p>
    </div>
  );
}
