/* =====================================================================
 * page_petty_cash.jsx — เงินสดย่อย (แท็บที่ 3 ของหน้า #fleet_card)
 * =====================================================================
 * งานจริงของผู้ดูแลวงเงินสดย่อยฝ่ายการเงิน (วงเงิน 20,000) — แทนไฟล์
 *   03.PETTY CASH/01.ทะเบียนคุม รายการเบิกเงินสดย่อย/STM+ทะเบียนคุม YYYY.xlsx
 *
 *   1. ผู้เบิกยื่นใบเบิก (เลขที่ MM-NN) → ผู้ดูแลโอนเงินให้เป็น "ยอดรวมต่อใบ"        → แถว kind 'pay'
 *   2. ผู้เบิกตั้ง OF ใน Mango แยกตามบิล มีฝ่ายกำกับ (ยอดรวมควรเท่าใบเบิก)       → pettyCashOf
 *   3. บัญชีจ่าย OF คืนเข้าวงเงินเป็นรอบ ๆ (ปกติกลาง/สิ้นเดือน) = "รับเงินสดย่อย"   → แถว kind 'in'
 *      ใบที่เงินกลับมาแล้ว → roundId = id ของแถว 'in' รอบนั้น
 *
 *   หน้านี้ตอบ: ใบไหนโอนไปแล้วยังไม่มีใครตั้ง OF · ใบไหนตั้ง OF แล้วแต่เงินยังไม่เข้า
 *              · เงินเข้ารอบนี้ (เช่น 31 เข้า 806) เป็นของใบไหนบ้าง · OF ไหนไม่มีใบจ่ายในทะเบียน
 *   สมการตรวจวงเงิน: เงินคงเหลือ + ค้างรับคืน = ยอดตั้งต้น + ดอกเบี้ย/รับอื่น
 *
 * จับคู่ใบจ่าย ↔ บรรทัด OF คำนวณสดทุกครั้ง (pcMatchOf) — เก็บเฉพาะที่ผู้ใช้ผูกเอง (ofIds)
 * ตัวอ่านรายงาน OF ใช้ของ Fleet Card (window.FCU · export ท้าย page_fleet_card.jsx)
 * Globals: React, Icon, Modal, WTPData, WTPAuth, WTPOverride, XLSX, fmtMoney, fmtDate
 * export: window.PettyCashPanel (+ window.PCU = ตัวคำนวณ ไว้ทดสอบ/ไล่ข้อมูล)
 * ===================================================================== */
(function () {
  'use strict';
  const { useState, useEffect, useMemo, useRef } = React;

  const PC_TXN = 'pettyCashTxns';
  const PC_OF = 'pettyCashOf';
  const PC_FUND = 'main';
  const PC_PAYEE_KEY = 'pc.ofPayees';        // override: ผู้รับเงินของ OF ที่นับเป็นเงินสดย่อย (คั่น |)
  const PC_ALIAS = 'pc.alias.';              // override: ผู้เบิก (ชื่อเล่น) → รหัสพนักงานที่ตั้ง OF
  const PC_UI = 'wtp.pc.ui';                 // localStorage ต่อคน: ปี / แท็บ / ตัวกรอง
  const PC_TXN_COLS = ['id', 'fund', 'kind', 'seq', 'payDate', 'checkDate', 'docNo', 'requester', 'description',
    'amount', 'deptCode', 'roundId', 'refDoc', 'ofNo', 'ofIds', 'status', 'scanNote', 'note', 'source', 'ym'];
  const PC_OF_COLS = ['id', 'ym', 'docNo', 'lineNo', 'expenseCode', 'expenseName', 'empCode', 'empName', 'amount',
    'itemName', 'vendor', 'invoiceNo', 'refDate', 'vchNo', 'vchDate', 'deptCode', 'deptName', 'acctName', 'acctNo',
    'docRemark', 'docAmount', 'docPaidAmount', 'appStatus', 'payStatus', 'pvNo', 'payDate', 'status', 'note'];
  const PC_OF_HEAD = ['docRemark', 'docAmount', 'docPaidAmount', 'appStatus', 'payStatus', 'pvNo', 'payDate', 'vchNo', 'vchDate'];
  const FCU = () => window.FCU || {};

  /* ── helpers ─────────────────────────────────────────────────────── */
  const pcNum = (v) => {
    if (v == null || v === '') return 0;
    const n = Number(String(v).replace(/[,\s฿]/g, ''));
    return isFinite(n) ? n : 0;
  };
  const pcTrim = (v) => String(v == null ? '' : v).trim();
  const pcR2 = (n) => Math.round(pcNum(n) * 100) / 100;
  const pcC = (n) => Math.round(pcNum(n) * 100);          // สตางค์ — เทียบยอดเป็นจำนวนเต็ม ไม่ให้ทศนิยมลอย
  const pcDept = (v) => pcTrim(v).toUpperCase();
  const pcMoney = (n) => fmtMoney(pcR2(n));
  const pcIds = (s) => String(s || '').split(',').map(x => x.trim()).filter(Boolean);
  const pcYear = (iso) => String(iso || '').slice(0, 4);
  const pcD = (iso) => (iso ? fmtDate(iso) : '—');
  const pcDs = (iso) => { const p = String(iso || '').split('-'); return p.length === 3 ? p[2] + '/' + p[1] : '—'; };
  const pcCan = (k) => !!(window.WTPAuth && window.WTPAuth.can && window.WTPAuth.can(k));
  const pcNewId = (tag) => 'pc|' + tag + '|' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
  const pcOfNorm = (v) => { const m = pcTrim(v).toUpperCase().replace(/\s+/g, '').match(/OF\d{6,}/); return m ? m[0] : ''; };
  const pcOfPaid = (l) => !!(l && (pcTrim(l.pvNo) || /^y/i.test(pcTrim(l.payStatus))));
  function pcToday() {
    const d = new Date();
    return new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().slice(0, 10);
  }
  function pcDays(a, b) {
    const ta = Date.parse(a + 'T00:00:00Z'), tb = Date.parse(b + 'T00:00:00Z');
    return (isNaN(ta) || isNaN(tb)) ? null : Math.round((tb - ta) / 86400000);
  }
  // Excel serial / Date / "dd/mm/yyyy" (พ.ศ. ได้) / ISO → 'YYYY-MM-DD'
  function pcISO(v) {
    if (v == null || v === '') return '';
    if (v instanceof Date && !isNaN(v.getTime())) {
      return new Date(v.getTime() - v.getTimezoneOffset() * 60000).toISOString().slice(0, 10);
    }
    if (typeof v === 'number') {
      if (v > 20000 && v < 80000) return new Date(Date.UTC(1899, 11, 30) + Math.round(v) * 86400000).toISOString().slice(0, 10);
      return '';
    }
    const s = pcTrim(v);
    if (/^\d{4}-\d{2}-\d{2}/.test(s)) return s.slice(0, 10);
    if (/^\d{5}(\.\d+)?$/.test(s)) return pcISO(Number(s));
    const m = s.match(/^(\d{1,2})[-/.](\d{1,2})[-/.](\d{2,4})/);
    if (m) {
      let y = Number(m[3]);
      if (m[3].length === 2) y += 2000;
      if (y > 2400) y -= 543;
      return y + '-' + String(+m[2]).padStart(2, '0') + '-' + String(+m[1]).padStart(2, '0');
    }
    return '';
  }
  function pcClean(row, cols) {
    const o = {};
    cols.forEach(k => {
      let v = row[k];
      if (v === undefined) v = null;
      if (k === 'amount' || k === 'docAmount' || k === 'docPaidAmount') v = (v === null || v === '') ? null : pcR2(v);
      if (k === 'seq' || k === 'lineNo') v = (v === null || v === '' || !isFinite(Number(v))) ? null : Math.round(Number(v));
      o[k] = v;
    });
    return o;
  }
  // ลำดับในทะเบียน: ปี → ลำดับ (seq) → วันที่ — คงเหลือสะสมเดินตามนี้ (เหมือนไฟล์ Excel)
  function pcCmp(a, b) {
    const ya = pcYear(a.payDate) || '0000', yb = pcYear(b.payDate) || '0000';
    if (ya !== yb) return ya < yb ? -1 : 1;
    const sa = a.seq == null || a.seq === '' ? 1e9 : Number(a.seq), sb = b.seq == null || b.seq === '' ? 1e9 : Number(b.seq);
    if (sa !== sb) return sa - sb;
    const da = a.payDate || '', db = b.payDate || '';
    if (da !== db) return da < db ? -1 : 1;
    return String(a.id) < String(b.id) ? -1 : 1;
  }
  const pcByDate = (a, b) => {
    const da = a.payDate || '', db = b.payDate || '';
    if (da !== db) return da < db ? -1 : 1;
    return pcCmp(a, b);
  };

  /* override (sync ทั้งทีม) — โหลดทั้งก้อนครั้งเดียวต่อ 5 วิ + ค่าที่เพิ่งเขียนเองอ่านได้ทันที */
  let pcOvAll = null, pcOvAt = 0;
  const pcOvMine = {};
  function pcOvGet(key) {
    if (Object.prototype.hasOwnProperty.call(pcOvMine, key)) return pcOvMine[key];
    const now = Date.now();
    if (!pcOvAll || now - pcOvAt > 5000) {
      let all = null;
      try { all = (window.WTPOverride && WTPOverride._load) ? WTPOverride._load() : null; } catch (_) {}
      pcOvAll = all || {}; pcOvAt = now;
    }
    return pcOvAll[key];
  }
  function pcOvSet(key, value) {
    const v = value == null ? '' : String(value);
    pcOvMine[key] = v;
    try { WTPOverride.setRaw(key, v || null); return true; } catch (_) { return false; }
  }
  function pcUiLoad() { try { return JSON.parse(localStorage.getItem(PC_UI) || '{}') || {}; } catch (_) { return {}; } }
  function pcUiSave(o) { try { localStorage.setItem(PC_UI, JSON.stringify(o)); } catch (_) {} }

  /* ผู้เบิก (ชื่อเล่นในทะเบียน) ↔ ผู้ตั้ง OF ใน Mango (ชื่อเต็ม/รหัสพนักงาน) — เรียนรู้จากที่ผู้ใช้ผูกเอง */
  const pcNick = (s) => pcTrim(s).replace(/^(พี่|น้อง|คุณ)\s*/, '').replace(/\s+/g, '').toLowerCase().slice(0, 30);
  const pcEmpKey = (l) => pcTrim(l && (l.empCode || l.empName));
  function pcAliasOf(nick) {
    const k = pcNick(nick);
    return k ? String(pcOvGet(PC_ALIAS + k) || '').split('|').filter(Boolean) : [];
  }
  function pcAliasLearn(nick, keys) {
    const k = pcNick(nick);
    if (!k) return;
    const cur = pcAliasOf(nick);
    const next = [...new Set((keys || []).filter(Boolean).concat(cur))].slice(0, 6);
    if (next.join('|') !== cur.join('|')) pcOvSet(PC_ALIAS + k, next.join('|'));
  }
  function pcAliasHit(nick, lines) {
    const a = pcAliasOf(nick);
    if (a.length && lines.some(l => a.indexOf(pcEmpKey(l)) >= 0)) return true;
    const n = pcTrim(nick).replace(/^(พี่|น้อง|คุณ)\s*/, '');
    return n.length >= 3 && lines.some(l => (pcTrim(l.itemName) + ' ' + pcTrim(l.docRemark) + ' ' + pcTrim(l.empName)).indexOf(n) >= 0);
  }

  /* ── ยอดเท่ากันไหม (สตางค์) — เผื่อ OF ตั้งยอดก่อน VAT 7% ── */
  function pcAmtEq(sumC, targetC) {
    if (Math.abs(sumC - targetC) <= 1) return true;
    return Math.abs(Math.round(sumC * 1.07) - targetC) <= 2;
  }
  /* หาชุดที่รวมได้เท่ายอด — ไล่ตามลำดับที่ส่งมา (ตัวหน้าได้ก่อน) · ตัดกิ่ง · จำกัดจำนวนก้าวกันค้าง */
  function pcSubset(items, targetC, valOf, maxSteps) {
    const arr = (items || []).map(x => ({ x, c: valOf(x) })).filter(o => o.c > 0);
    const n = arr.length;
    if (!n || targetC <= 0) return null;
    const suffix = new Array(n + 1).fill(0);
    for (let i = n - 1; i >= 0; i--) suffix[i] = suffix[i + 1] + arr[i].c;
    if (suffix[0] < targetC - 1) return null;
    const cap = maxSteps || 50000;
    let steps = 0, found = null;
    const pick = [];
    const dfs = (i, rest) => {
      if (found || steps++ > cap) return;
      if (Math.abs(rest) <= 1) { found = pick.slice(); return; }
      if (i >= n || rest < -1 || suffix[i] < rest - 1) return;
      pick.push(arr[i]); dfs(i + 1, rest - arr[i].c); pick.pop();
      if (!found) dfs(i + 1, rest);
    };
    dfs(0, targetC);
    return found ? found.map(o => o.x) : null;
  }

  /* ── จับคู่ใบจ่าย ↔ บรรทัด OF ─────────────────────────────────────
   *   1. ผู้ใช้ผูกเอง (ofIds)
   *   2. เลข OF ที่จดไว้ในใบ (ofNo) — ใบ OF เดียวครอบหลายใบเบิกได้ → แบ่งบรรทัดตามยอดของแต่ละใบ
   *   3. ระบบจับเอง: ยอดรวม (ทั้งใบ OF / เฉพาะบรรทัดฝ่ายเดียวกัน / บางบรรทัด) = ยอดใบเบิก
   *      + ฝ่ายตรง + ผู้ตั้ง OF ตรงกับผู้เบิก (ที่เคยผูก) + วันที่ OF ไม่ห่างจากวันจ่ายเกินไป */
  function pcMatchOf(pays, lines) {
    const byId = {}, docs = {};
    lines.forEach(l => { byId[l.id] = l; (docs[l.docNo] = docs[l.docNo] || []).push(l); });
    const used = {}, res = {};
    const take = (p, ls, by) => { ls.forEach(l => { used[l.id] = p.id; }); res[p.id] = { lines: ls, by }; };
    const free = (dn) => (docs[dn] || []).filter(l => !used[l.id]);
    const lc = (l) => pcC(l.amount);
    const sumC = (ls) => ls.reduce((s, l) => s + lc(l), 0);
    pays.forEach(p => {
      const ids = pcIds(p.ofIds);
      if (ids.length) take(p, ids.map(id => byId[id]).filter(l => l && !used[l.id]), 'manual');
    });
    const byOf = {};
    pays.forEach(p => {
      if (res[p.id]) return;
      const n = pcOfNorm(p.ofNo);
      if (n && docs[n]) (byOf[n] = byOf[n] || []).push(p);
    });
    Object.keys(byOf).forEach(n => {
      const ps = byOf[n];
      if (ps.length === 1) { const pool = free(n); if (pool.length) take(ps[0], pool, 'ofno'); return; }
      ps.forEach(p => {
        const pool = free(n);
        const same = pool.filter(l => pcDept(l.deptCode) === pcDept(p.deptCode));
        const sub = (same.length ? pcSubset(same, pcC(p.net), lc, 20000) : null) || pcSubset(pool, pcC(p.net), lc, 20000);
        if (sub) take(p, sub, 'ofno');
      });
      const left = ps.filter(p => !res[p.id]), rest = free(n);
      if (left.length === 1 && rest.length) take(left[0], rest, 'ofno');
    });
    const docList = Object.keys(docs).map(dn => ({ dn, date: docs[dn][0].refDate || docs[dn][0].vchDate || '' }));
    pays.forEach(p => {
      if (res[p.id] || p.status === 'ignored') return;
      const target = pcC(p.net);
      if (target <= 0) return;
      const pd = pcDept(p.deptCode);
      let best = null;
      docList.forEach(d => {
        const pool = free(d.dn);
        if (!pool.length) return;
        const dd = (d.date && p.payDate) ? pcDays(p.payDate, d.date) : null;
        if (dd != null && (dd < -45 || dd > 150)) return;
        const same = pd ? pool.filter(l => pcDept(l.deptCode) === pd) : [];
        const wholeDoc = pool.length === docs[d.dn].length;
        let cand = null, whole = false;
        if (pcAmtEq(sumC(pool), target) || (wholeDoc && Math.abs(pcC(pool[0].docAmount) - target) <= 1)) { cand = pool; whole = wholeDoc; }
        else if (same.length && same.length < pool.length && pcAmtEq(sumC(same), target)) cand = same;
        else if (same.length > 1 && sumC(same) > target) cand = pcSubset(same, target, lc, 4000);
        if (!cand || !cand.length) return;
        let score = 40;
        if (pd && cand.every(l => pcDept(l.deptCode) === pd)) score += 30;
        else if (!pd) score += 10;
        if (pcAliasHit(p.requester, cand)) score += 25;
        if (whole) score += 8;
        if (dd != null) score -= Math.min(25, Math.abs(dd - 7) / 4);
        if (!best || score > best.score) best = { score, lines: cand };
      });
      if (best && best.score >= 50) take(p, best.lines, 'auto');
    });
    return { res, used };
  }

  /* ── คำนวณทุกอย่างของหน้าจากข้อมูลดิบ ─────────────────────────── */
  function pcBuild(txnsAll, ofsAll, today) {
    const rows = (txnsAll || []).filter(t => t && t.kind).slice().sort(pcCmp);
    const ofs = (ofsAll || []).filter(Boolean);
    const inById = {};
    rows.forEach(t => { if (t.kind === 'in') inById[t.id] = t; });
    const pays0 = rows.filter(t => t.kind === 'pay');
    // เงินทอนคืน → หักจากใบเลขเดียวกันที่จ่ายก่อนวันรับคืน (ยอดที่ต้องได้คืนจริง = จ่ายออก − เงินทอน)
    const refundOf = {}, refundLinked = {};
    rows.filter(t => t.kind === 'refund').forEach(r => {
      const doc = pcTrim(r.refDoc);
      if (!doc) return;
      let hit = null;
      pays0.forEach(p => {
        if (pcTrim(p.docNo) !== doc) return;
        if (r.payDate && p.payDate && p.payDate > r.payDate) return;
        if (!hit || (p.payDate || '') >= (hit.payDate || '')) hit = p;
      });
      if (hit) { refundOf[hit.id] = (refundOf[hit.id] || 0) + pcNum(r.amount); refundLinked[r.id] = hit.id; }
    });
    const pays = pays0.map(p => Object.assign({}, p, {
      gross: pcR2(p.amount), refund: pcR2(refundOf[p.id] || 0), net: pcR2(pcNum(p.amount) - (refundOf[p.id] || 0)),
    }));
    const ofLive = ofs.filter(l => l.status !== 'ignored');
    const m = pcMatchOf(pays.filter(p => p.status !== 'ignored').slice().sort(pcByDate), ofLive);
    const payById = {};
    pays.forEach(p => {
      payById[p.id] = p;
      const r = m.res[p.id];
      p.ofLines = r ? r.lines : [];
      p.matchBy = r ? r.by : '';
      p.ofSum = pcR2(p.ofLines.reduce((s, l) => s + pcNum(l.amount), 0));
      p.ofDocs = [...new Set(p.ofLines.map(l => l.docNo))];
      p.ofDepts = [...new Set(p.ofLines.map(l => pcDept(l.deptCode)).filter(Boolean))];
      p.ofPaid = p.ofLines.length > 0 && p.ofLines.every(pcOfPaid);
      p.ofPayDate = p.ofPaid ? (p.ofLines.map(l => l.payDate).filter(Boolean).sort().pop() || '') : '';
      p.ofDiffOk = p.ofLines.length ? pcAmtEq(pcC(p.ofSum), pcC(p.net)) : true;
      p.ofDiff = p.ofLines.length ? pcR2(p.ofSum - p.net) : 0;
      p.round = p.roundId ? (inById[p.roundId] || null) : null;
      p.age = p.payDate ? pcDays(p.payDate, today) : null;
      p.state = p.status === 'ignored' ? 'ignored'
        : p.round ? 'in'
        : p.ofLines.length ? (p.ofPaid ? 'ofpaid' : 'of')
        : pcOfNorm(p.ofNo) ? 'ofnote'
        : 'none';
    });
    const rounds = rows.filter(t => t.kind === 'in').map(t => {
      const items = pays.filter(p => p.roundId === t.id).sort(pcByDate);
      const itemsNet = pcR2(items.reduce((s, p) => s + p.net, 0));
      return Object.assign({}, t, { items, itemsNet, diff: pcR2(pcNum(t.amount) - itemsNet) });
    });
    // คงเหลือสะสม — ปีที่มีแถวตั้งวงเงิน/ยอดยกมา เริ่มนับใหม่ (ไฟล์ทะเบียนคุมขึ้นปีใหม่ด้วยยอดยกมา)
    const openYears = {};
    rows.forEach(t => { if (t.kind === 'open') openYears[pcYear(t.payDate)] = true; });
    const balOf = {}, yearInfo = {};
    let bal = 0, curY = '';
    rows.forEach(t => {
      const y = pcYear(t.payDate) || curY || '0000';
      if (y !== curY) {
        if (curY) yearInfo[curY].close = bal;
        if (openYears[y]) bal = 0;
        yearInfo[y] = { carry: bal, hasOpen: !!openYears[y], close: bal };
        curY = y;
      }
      bal = pcR2(bal + (t.kind === 'pay' ? -1 : 1) * pcNum(t.amount));
      balOf[t.id] = bal;
    });
    if (curY) yearInfo[curY].close = bal;
    // OF ที่ไม่ได้ผูกกับใบจ่ายใด → จัดเป็นรายใบ OF
    const lineOwner = m.used;
    const docMap = {};
    ofs.forEach(l => {
      const d = docMap[l.docNo] || (docMap[l.docNo] = { docNo: l.docNo, ym: l.ym, lines: [], amount: 0, date: l.refDate || l.vchDate || '',
        emp: l.empName || '', depts: {}, pvNo: '', payDate: '', paid: true, ignored: true, owners: {} });
      d.lines.push(l);
      d.amount = pcR2(d.amount + pcNum(l.amount));
      if (l.deptCode) d.depts[pcDept(l.deptCode)] = 1;
      if (l.pvNo) d.pvNo = l.pvNo;
      if (l.payDate) d.payDate = l.payDate;
      if (!pcOfPaid(l)) d.paid = false;
      if (l.status !== 'ignored') d.ignored = false;
      if (lineOwner[l.id]) d.owners[lineOwner[l.id]] = 1;
    });
    const ofDocs = Object.values(docMap).map(d => {
      const live = d.lines.filter(l => l.status !== 'ignored');
      const freeLines = live.filter(l => !lineOwner[l.id]);
      return Object.assign(d, {
        depts: Object.keys(d.depts), owners: Object.keys(d.owners).map(id => payById[id]).filter(Boolean),
        freeLines, freeAmt: pcR2(freeLines.reduce((s, l) => s + pcNum(l.amount), 0)),
        state: d.ignored ? 'ignored' : !freeLines.length ? 'linked' : freeLines.length === live.length ? 'orphan' : 'partial',
      });
    }).sort((a, b) => (a.docNo < b.docNo ? 1 : -1));
    return { rows, pays, payById, inById, rounds, balOf, yearInfo, refundLinked, ofDocs, lineOwner, ofs };
  }

  /* สมการวงเงินของปี Y: เงินคงเหลือ + ค้างรับคืน (+ ใบที่ไม่ต้องตั้ง OF) = ยอดตั้งต้น + ดอกเบี้ย/รับอื่น
   *   ผลต่าง ≠ 0 แปลว่ามีรอบเงินเข้าที่ยอดไม่ตรงกับใบที่ผูกไว้ (หรือรับคืนเงินทอนที่หาใบไม่เจอ) */
  function pcEquation(b, Y) {
    const info = b.yearInfo[Y];
    if (!info) return null;
    const yEnd = Y + '-12-31', prevEnd = (Number(Y) - 1) + '-12-31';
    const inY = (t) => pcYear(t.payDate) === Y;
    const sumAmt = (ts) => pcR2(ts.reduce((s, t) => s + pcNum(t.amount), 0));
    const outAt = (end) => pcR2(b.pays.filter(p => p.status !== 'ignored' && (p.payDate || '') <= end
      && (!p.round || (p.round.payDate || '') > end)).reduce((s, p) => s + p.net, 0));
    const opens = b.rows.filter(t => t.kind === 'open' && inY(t));
    const other = sumAmt(b.rows.filter(t => t.kind === 'other' && inY(t)));
    const refundUn = sumAmt(b.rows.filter(t => t.kind === 'refund' && inY(t) && !b.refundLinked[t.id]));
    const startCash = info.hasOpen ? sumAmt(opens) : info.carry;
    const startOut = outAt(prevEnd);
    const out = outAt(yEnd);
    const ignored = pcR2(b.pays.filter(p => p.status === 'ignored' && inY(p)).reduce((s, p) => s + p.net, 0));
    const cash = info.close;
    const expected = pcR2(startCash + startOut + other + refundUn);
    const actual = pcR2(cash + out + ignored);
    const limitRow = opens.filter(t => /วงเงิน/.test(t.description || '')).pop();
    const fundLimit = limitRow ? pcR2(limitRow.amount) : 20000;
    const roundDiff = pcR2(b.rounds.filter(inY).reduce((s, r) => s + r.diff, 0));
    return { Y, cash, out, ignored, startCash, startOut, other, refundUn, expected, actual, gap: pcR2(actual - expected), fundLimit, roundDiff };
  }

  /* เงินเข้ารอบนี้ (ยอด / วันที่) น่าจะเป็นของใบไหน
   *   1. OF ที่บัญชีจ่ายใกล้วันเงินเข้า (±5 วัน)   2. ใบที่ค้างเก่าสุดเรียงต่อกัน
   *   3. ชุดที่รวมได้พอดี (ใบที่ตั้ง OF แล้วก่อน · ใบเก่าก่อน) */
  function pcPropose(amount, date, cands) {
    const target = pcC(amount);
    if (target <= 0 || !cands.length) return null;
    const val = (p) => pcC(p.net);
    const tot = (ps) => ps.reduce((s, p) => s + val(p), 0);
    const near = cands.filter(p => p.ofPayDate && date && Math.abs(pcDays(p.ofPayDate, date)) <= 5);
    if (near.length) {
      if (Math.abs(tot(near) - target) <= 1) return { ids: near.map(p => p.id), why: 'ตรงกับ OF ที่บัญชีจ่ายวันที่ ' + pcD(near[0].ofPayDate) + ' ทั้งหมด ' + near.length + ' ใบ' };
      const sub = pcSubset(near, target, val, 20000);
      if (sub) return { ids: sub.map(p => p.id), why: 'OF ที่บัญชีจ่ายใกล้วันเงินเข้า ' + sub.length + ' ใบ รวมได้พอดี' };
    }
    let acc = 0;
    for (let i = 0; i < cands.length; i++) {
      acc += val(cands[i]);
      if (Math.abs(acc - target) <= 1) return { ids: cands.slice(0, i + 1).map(p => p.id), why: 'ใบที่ค้างเก่าสุด ' + (i + 1) + ' ใบแรก รวมได้พอดี' };
      if (acc > target + 1) break;
    }
    const ordered = cands.filter(p => p.state !== 'none').concat(cands.filter(p => p.state === 'none'));
    const sub = pcSubset(ordered, target, val, 150000);
    if (sub) return { ids: sub.map(p => p.id), why: 'หาชุดที่รวมได้พอดี ' + sub.length + ' ใบ — ตรวจก่อนบันทึก' };
    return null;
  }

  /* ── อ่านไฟล์ทะเบียนคุม (STM+ทะเบียนคุม YYYY.xlsx) ─────────────────
   *   หัวตาราง: วันที่ตรวจเอกสาร | วันที่จ่ายเงิน | เลขที่เอกสาร | ผู้เบิก | รายการ | รับ | จ่าย | คงเหลือ | หมายเหตุ(ฝ่าย)
   *   ขวาของหมายเหตุ (ไม่มีหัว): 1 = เบิกแล้ว · วันที่เงินเข้ารอบ · เลข OF · สถานะไฟล์สแกน → แยกด้วยเนื้อหาในคอลัมน์ */
  function pcFindRegisterHeader(aoa) {
    const txt = (v) => pcTrim(v).replace(/\s+/g, ' ');
    for (let i = 0; i < Math.min((aoa || []).length, 80); i++) {
      const r = (aoa[i] || []).map(txt);
      if (r.some(c => c === 'เลขที่เอกสาร') && r.some(c => /^วันที่จ่าย/.test(c))) return i;
    }
    return -1;
  }
  function pcParseRegister(aoa) {
    const out = { rows: [], year: '', fixes: [], error: '', fundLimit: 0, holder: '' };
    const txt = (v) => pcTrim(v).replace(/\s+/g, ' ');
    const hr = pcFindRegisterHeader(aoa);
    if (hr < 0) { out.error = 'ไม่พบหัวตาราง (เลขที่เอกสาร / วันที่จ่ายเงิน / ผู้เบิก …)'; return out; }
    const H = (aoa[hr] || []).map(txt);
    const find = (re) => H.findIndex(h => re.test(h));
    const C = { check: find(/^วันที่ตรวจ/), pay: find(/^วันที่จ่าย/), doc: find(/^เลขที่เอกสาร/), who: find(/^ผู้เบิก/),
      desc: find(/^รายการ$/), inn: find(/^รับ$/), out: find(/^จ่าย$/), bal: find(/^คงเหลือ$/), note: find(/^หมายเหตุ|^ฝ่าย$/) };
    if (C.pay < 0 || C.out < 0 || C.inn < 0) { out.error = 'หัวตารางไม่ครบ — ต้องมี วันที่จ่ายเงิน / รับ / จ่าย'; return out; }
    for (let i = 0; i < hr; i++) (aoa[i] || []).forEach(c => {
      const s = txt(c);
      let m = s.match(/วงเงิน[.\s]*([\d,]+(?:\.\d+)?)/);
      if (m && !out.fundLimit) out.fundLimit = pcNum(m[1]);
      m = s.match(/ผู้ดูแลเงินสดย่อย[.\s]*(.+)/);
      if (m && !out.holder) out.holder = m[1].replace(/\.{2,}/g, ' ').replace(/\s+/g, ' ').trim();
    });
    const base = Math.max(C.note, C.bal, C.out, C.inn) + 1;
    const stats = {};
    for (let i = hr + 1; i < aoa.length; i++) {
      const r = aoa[i] || [];
      for (let j = base; j < Math.min(r.length, base + 8); j++) {
        const v = r[j];
        if (v == null || v === '') continue;
        const s = stats[j] || (stats[j] = { date: 0, of: 0, scan: 0, one: 0 });
        if ((typeof v === 'number' && v > 40000 && v < 60000) || v instanceof Date) s.date++;
        else if (/^OF\d{6,}/i.test(txt(v)) || /^OK$/i.test(txt(v))) s.of++;
        else if (/สแกน/.test(txt(v))) s.scan++;
        else if (v === 1 || txt(v) === '1') s.one++;
      }
    }
    const best = (k) => { let bj = -1, bn = 0; Object.keys(stats).forEach(j => { if (stats[j][k] > bn) { bn = stats[j][k]; bj = Number(j); } }); return bj; };
    C.round = best('date'); C.of = best('of'); C.scan = best('scan');
    const g = (r, j) => (j >= 0 && j < r.length ? r[j] : '');
    let seq = 0, started = false;
    for (let i = hr + 1; i < aoa.length; i++) {
      const r = aoa[i] || [];
      if ([C.pay, C.doc, C.check, C.who].some(j => /ยอดคงเหลือ/.test(txt(g(r, j))))) break;
      const inAmt = pcNum(g(r, C.inn)), outAmt = pcNum(g(r, C.out));
      const who = txt(g(r, C.who)), desc = txt(g(r, C.desc)), note = txt(g(r, C.note));
      const payDate = pcISO(g(r, C.pay)), checkDate = pcISO(g(r, C.check));
      if (!inAmt && !outAmt) {
        const bal = pcNum(g(r, C.bal));
        if (!started && bal && (who || note || desc)) {
          seq++;
          out.rows.push({ kind: 'open', seq, payDate, checkDate: '', docNo: '', requester: '', deptCode: '', amount: pcR2(bal),
            description: 'ยอดยกมา' + ((who || desc || note) ? ' (' + (who || desc || note) + ')' : ''), _openBal: true });
          started = true;
        }
        continue;
      }
      started = true;
      seq++;
      if (outAmt) {
        const ofCell = txt(g(r, C.of));
        out.rows.push({ kind: 'pay', seq, payDate, checkDate, docNo: txt(g(r, C.doc)), requester: who, description: desc,
          amount: pcR2(outAmt), deptCode: pcDept(note), ofNo: pcOfNorm(ofCell), scanNote: txt(g(r, C.scan)),
          note: /^OK$/i.test(ofCell) ? 'OK' : '', _round: pcISO(g(r, C.round)) });
      } else {
        const t = who || desc;
        let kind = 'other', refDoc = '';
        if (/รับเงินสดย่อย|เบิกชดเชย|รับชดเชย/.test(t)) kind = 'in';
        else if (/ตั้งวงเงิน|ยอดยกมา|ยกมา/.test(t)) kind = 'open';
        else if (/รับคืน|เงินทอน/.test(t)) {
          const mm = t.match(/(\d{2}-\d{1,3}(?:\.\d+)?)/);
          refDoc = mm ? mm[1] : '';
          kind = refDoc ? 'refund' : 'other';
        }
        out.rows.push({ kind, seq, payDate, checkDate, docNo: txt(g(r, C.doc)), requester: '', deptCode: '',
          description: t || PC_KINDS[kind].label, amount: pcR2(inAmt), refDoc, scanNote: txt(g(r, C.scan)) });
      }
    }
    const pays = out.rows.filter(r => r.kind === 'pay');
    if (!pays.length && !out.rows.some(r => r.kind === 'in')) { out.error = 'อ่านได้ 0 รายการ — ตรวจว่าเลือกชีตถูก'; return out; }
    const yc = {};
    pays.forEach(p => { const y = pcYear(p.payDate); if (y) yc[y] = (yc[y] || 0) + 1; });
    out.year = Object.keys(yc).sort((a, b) => yc[b] - yc[a])[0] || pcYear(pcToday());
    // วันที่ของแถวตั้งวงเงิน/ยอดยกมา: ไฟล์จริงพิมพ์ทีหลัง (09/09/2026 ทั้งที่เป็นยอดต้นปี) → ใช้ 1 ม.ค. ของปีนั้น
    const firstDate = out.rows.filter(r => r.kind !== 'open' && r.payDate).map(r => r.payDate).sort()[0] || '';
    out.rows.filter(r => r.kind === 'open').forEach(r => {
      if (!r.payDate || (firstDate && r.payDate > firstDate) || pcYear(r.payDate) !== out.year) {
        if (r.payDate && r.payDate !== out.year + '-01-01') out.fixes.push({ kind: 'date', text: '"' + r.description + '" ในไฟล์ลงวันที่ ' + pcD(r.payDate) + ' (หลังรายการแรกของปี) → ใช้ 01/01/' + out.year });
        r.payDate = out.year + '-01-01';
      }
    });
    if (!out.fundLimit) {
      const lim = out.rows.find(r => r.kind === 'open' && /วงเงิน/.test(r.description));
      if (lim) out.fundLimit = lim.amount;
    }
    out.rows.filter(r => r.kind === 'refund').forEach(rf => {
      const hit = pays.filter(p => p.docNo === rf.refDoc && (!rf.payDate || !p.payDate || p.payDate <= rf.payDate)).pop();
      if (hit) hit._refund = pcR2((hit._refund || 0) + rf.amount);
    });
    pays.forEach(p => { p._from = p._round || ''; });
    pcFixRounds(pays, out.rows.filter(r => r.kind === 'in'), out.fixes);
    return out;
  }
  /* ตรวจรอบเงินเข้าในไฟล์: ใบที่ติดวันที่รอบไว้ต้องรวมได้เท่ายอดเงินเข้า
   *   ไฟล์จริงปี 2026 มี 3 จุดที่ไม่ตรง (แก้ให้แล้วตรงทุกรอบ):
   *     - รอบ 15/05 และ 31/05 ไม่มีใบติดเลย — ใบของรอบนั้นไปติดวันที่ 15/06 / 30/06 แทน
   *       → ย้ายกลุ่มใบของรอบถัดไปที่ "จ่ายก่อนวันเงินเข้า" ถ้ารวมได้เท่ายอดพอดี
   *     - รอบ 31/10 (806) ยังไม่ได้ติ๊กใบ → ใบที่ค้างเก่าสุด 5 ใบแรกรวมได้ 806 พอดี */
  function pcFixRounds(pays, ins, fixes) {
    const net = (p) => pcC(p.amount) - pcC(p._refund || 0);
    const tot = (ps) => ps.reduce((s, p) => s + net(p), 0);
    const done = new Set();
    ins.slice().sort(pcByDate).forEach(R => {
      const T = pcC(R.amount), D = R.payDate;
      const mine = pays.filter(p => p._round === D && !done.has(p));
      if (Math.abs(tot(mine) - T) <= 1) { mine.forEach(p => done.add(p)); return; }
      const close = (group, why) => {
        const moved = group.filter(p => p._from !== D), dropped = mine.filter(p => group.indexOf(p) < 0);
        group.forEach(p => { p._round = D; done.add(p); });
        dropped.forEach(p => { p._round = ''; });
        fixes.push({ kind: 'round', date: D, amount: R.amount, why, items: group.length,
          moved: moved.map(p => ({ docNo: p.docNo, amount: p.amount, from: p._from })),
          dropped: dropped.map(p => ({ docNo: p.docNo, amount: p.amount })) });
      };
      const later = pays.filter(p => !done.has(p) && p._round !== D && (!p._round || p._round > D)
        && p.payDate && p.payDate <= D).sort(pcByDate);
      const labels = [...new Set(later.map(p => p._round || ''))].sort((a, b) => ((a || '9999') < (b || '9999') ? -1 : 1));
      for (let k = 0; k < labels.length; k++) {
        const L = labels[k];
        const grp = mine.concat(later.filter(p => (p._round || '') === L));
        if (Math.abs(tot(grp) - T) <= 1) {
          close(grp, L ? 'ในไฟล์ติดวันที่รอบ ' + pcD(L) + ' ไว้ — เป็นใบที่จ่ายก่อนวันเงินเข้ารอบนี้ รวมได้เท่ายอดพอดี' : 'ในไฟล์ยังไม่ได้ติ๊กว่าใบไหนเข้ารอบนี้ — ใบที่ค้างรวมได้เท่ายอดพอดี');
          return;
        }
      }
      const sub = pcSubset(mine.concat(later), T, net, 200000);
      if (sub) { close(sub, 'เลือกใบที่รวมได้เท่ายอดเงินเข้า (ใบเก่าก่อน)'); return; }
      mine.forEach(p => done.add(p));
      fixes.push({ kind: 'round', date: D, amount: R.amount, unfixed: true, items: mine.length,
        why: 'หาใบที่รวมได้เท่ายอดเงินเข้าไม่เจอ — ต่าง ' + pcMoney((T - tot(mine)) / 100) + ' (คงไว้ตามไฟล์)' });
    });
  }

  const PC_KINDS = {
    pay: { label: 'จ่ายออก' },
    in: { label: 'เงินเข้ารอบ (บัญชีจ่ายคืน)' },
    open: { label: 'ตั้งวงเงิน / ยอดยกมา' },
    other: { label: 'ดอกเบี้ยรับ / รับอื่น' },
    refund: { label: 'รับคืนเงินทอน' },
  };
  const PC_STATE = {
    none: { label: 'ยังไม่ตั้ง OF', tone: 'bad' },
    ofnote: { label: 'จดเลข OF ไว้ · รอเงินเข้า', tone: 'warn' },
    of: { label: 'ตั้ง OF แล้ว · รอเงินเข้า', tone: 'warn' },
    ofpaid: { label: 'OF จ่ายแล้ว · ยังไม่บันทึกเงินเข้า', tone: 'info' },
    in: { label: 'เงินเข้าแล้ว', tone: 'ok' },
    ignored: { label: 'ไม่ต้องตั้ง OF', tone: 'mute' },
  };
  const PC_WAIT = { ofnote: 1, of: 1, ofpaid: 1 };
  const PC_MONTHS = ['ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', 'มิ.ย.', 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', 'พ.ย.', 'ธ.ค.'];
  const pcIsOpen = (p) => p.state !== 'in' && p.state !== 'ignored';

  function pcNextDocNo(pays, dateISO, extra) {
    const y = pcYear(dateISO), mm = String(dateISO || '').slice(5, 7);
    if (!y || !mm) return '';
    let max = 0;
    (pays || []).concat(extra || []).forEach(p => {
      if (pcYear(p.checkDate || p.payDate) !== y) return;
      const m = pcTrim(p.docNo).match(/^(\d{2})-(\d{1,3})/);
      if (m && m[1] === mm) max = Math.max(max, Number(m[2]));
    });
    return mm + '-' + String(max + 1).padStart(2, '0');
  }

  /* ── Excel ─────────────────────────────────────────────────────── */
  function pcExportExcel(b, Y, eq) {
    const U = FCU();
    if (typeof XLSX === 'undefined' || !U.xlSheet) { alert('ระบบ Export ยังไม่พร้อม — ลองรีเฟรชหน้า'); return; }
    const inY = (t) => pcYear(t.payDate) === Y;
    const reg = b.rows.filter(inY).map((t, i) => {
      const p = t.kind === 'pay' ? b.payById[t.id] : null;
      const r = t.kind === 'in' ? b.rounds.find(x => x.id === t.id) : null;
      return {
        seq: t.seq || i + 1, check: t.checkDate ? pcD(t.checkDate) : '', pay: pcD(t.payDate), doc: t.docNo || '',
        who: t.kind === 'pay' ? (t.requester || '') + (t.description ? ' · ' + t.description : '') : (t.description || PC_KINDS[t.kind].label),
        inn: t.kind === 'pay' ? '' : pcR2(t.amount), out: t.kind === 'pay' ? pcR2(t.amount) : '', bal: b.balOf[t.id],
        dept: t.deptCode || '', round: p && p.round ? pcD(p.round.payDate) : '',
        of: p ? (p.ofDocs.join(', ') || pcOfNorm(p.ofNo)) : '',
        state: p ? PC_STATE[p.state].label : r ? (r.items.length + ' ใบ' + (Math.abs(r.diff) >= 0.01 ? ' · ต่าง ' + pcMoney(r.diff) : ' · ตรง')) : PC_KINDS[t.kind].label,
        scan: t.scanNote || '', note: t.note || '',
      };
    });
    const sumIn = pcR2(reg.reduce((s, r) => s + pcNum(r.inn), 0)), sumOut = pcR2(reg.reduce((s, r) => s + pcNum(r.out), 0));
    const wb = XLSX.utils.book_new();
    XLSX.utils.book_append_sheet(wb, U.xlSheet({
      title: 'ทะเบียนคุมเงินสดย่อย ปี ' + Y,
      note: 'เงินคงเหลือ ' + pcMoney(eq.cash) + ' + ค้างรับคืน ' + pcMoney(eq.out) + ' = ' + pcMoney(eq.actual)
        + ' · ยอดตั้งต้น+ดอกเบี้ย/รับอื่น ' + pcMoney(eq.expected) + (Math.abs(eq.gap) >= 0.01 ? ' · ต่าง ' + pcMoney(eq.gap) : ' · ลงตัว'),
      columns: [
        { key: 'seq', label: 'ลำดับ', type: 'int', width: 6 }, { key: 'check', label: 'วันที่ตรวจเอกสาร', width: 12 },
        { key: 'pay', label: 'วันที่จ่ายเงิน', width: 12 }, { key: 'doc', label: 'เลขที่เอกสาร', width: 10 },
        { key: 'who', label: 'ผู้เบิก / รายการ', width: 26 }, { key: 'inn', label: 'รับ', type: 'number', width: 12 },
        { key: 'out', label: 'จ่าย', type: 'number', width: 12 }, { key: 'bal', label: 'คงเหลือ', type: 'number', width: 12 },
        { key: 'dept', label: 'ฝ่าย', width: 7 }, { key: 'round', label: 'เงินเข้ารอบ', width: 12 },
        { key: 'of', label: 'เลข OF', width: 16 }, { key: 'state', label: 'สถานะ', width: 26 },
        { key: 'scan', label: 'ไฟล์สแกน', width: 22 }, { key: 'note', label: 'หมายเหตุ', width: 18 },
      ],
      rows: reg, total: { inn: sumIn, out: sumOut },
    }), 'ทะเบียนคุม ' + Y);
    const rounds = b.rounds.filter(inY).map(r => ({
      date: pcD(r.payDate), amount: pcR2(r.amount), n: r.items.length, items: r.itemsNet, diff: r.diff,
      docs: r.items.map(p => p.docNo).join(', '), desc: r.description || '',
    }));
    XLSX.utils.book_append_sheet(wb, U.xlSheet({
      title: 'รอบเงินเข้า ปี ' + Y, note: 'แต่ละรอบเป็นเงินของใบไหนบ้าง · ผลต่าง = ยอดเงินเข้า − ยอดใบที่ผูก',
      columns: [
        { key: 'date', label: 'วันที่เงินเข้า', width: 12 }, { key: 'amount', label: 'ยอดเงินเข้า', type: 'number', width: 13 },
        { key: 'n', label: 'จำนวนใบ', type: 'int', width: 8 }, { key: 'items', label: 'ยอดใบที่ผูก', type: 'number', width: 13 },
        { key: 'diff', label: 'ผลต่าง', type: 'number', width: 11 }, { key: 'docs', label: 'เลขที่เอกสาร', width: 60 },
        { key: 'desc', label: 'รายการ', width: 18 },
      ],
      rows: rounds, total: { amount: pcR2(rounds.reduce((s, r) => s + r.amount, 0)), items: pcR2(rounds.reduce((s, r) => s + r.items, 0)) },
    }), 'รอบเงินเข้า');
    const open = b.pays.filter(p => pcIsOpen(p) && pcYear(p.payDate) <= Y).sort(pcByDate).map(p => ({
      pay: pcD(p.payDate), doc: p.docNo, who: p.requester, dept: p.deptCode, amount: p.net, age: p.age,
      state: PC_STATE[p.state].label, of: p.ofDocs.join(', ') || pcOfNorm(p.ofNo), ofSum: p.ofLines.length ? p.ofSum : '',
    }));
    XLSX.utils.book_append_sheet(wb, U.xlSheet({
      title: 'ค้างรับคืน (ยังไม่ได้เงินเข้า)', note: 'ยังไม่ตั้ง OF = ต้องตามผู้เบิก · ตั้ง OF แล้ว = รอบัญชีจ่ายคืน',
      columns: [
        { key: 'pay', label: 'วันที่จ่าย', width: 12 }, { key: 'doc', label: 'เลขที่', width: 10 }, { key: 'who', label: 'ผู้เบิก', width: 14 },
        { key: 'dept', label: 'ฝ่าย', width: 7 }, { key: 'amount', label: 'ยอด', type: 'number', width: 12 },
        { key: 'age', label: 'ผ่านมา (วัน)', type: 'int', width: 10 }, { key: 'state', label: 'สถานะ', width: 30 },
        { key: 'of', label: 'เลข OF', width: 18 }, { key: 'ofSum', label: 'ยอด OF', type: 'number', width: 12 },
      ],
      rows: open, total: { amount: pcR2(open.reduce((s, r) => s + pcNum(r.amount), 0)) },
    }), 'ค้างรับคืน');
    const dept = pcDeptSummary(b, Y);
    XLSX.utils.book_append_sheet(wb, U.xlSheet({
      title: 'สรุปรายฝ่าย ปี ' + Y, note: 'ยอดจ่ายออก = ยอดใบเบิก (หักเงินทอนคืนแล้ว)',
      columns: [
        { key: 'dept', label: 'ฝ่าย', width: 9 }, { key: 'n', label: 'ใบ', type: 'int', width: 7 },
        { key: 'total', label: 'จ่ายออก', type: 'number', width: 13 }, { key: 'back', label: 'เงินเข้าแล้ว', type: 'number', width: 13 },
        { key: 'wait', label: 'ตั้ง OF แล้ว รอเงินเข้า', type: 'number', width: 16 }, { key: 'none', label: 'ยังไม่ตั้ง OF', type: 'number', width: 13 },
      ],
      rows: dept, total: ['n', 'total', 'back', 'wait', 'none'].reduce((o, k) => { o[k] = pcR2(dept.reduce((s, r) => s + r[k], 0)); return o; }, {}),
    }), 'สรุปรายฝ่าย');
    const orphan = b.ofDocs.filter(d => (d.state === 'orphan' || d.state === 'partial') && pcYear(d.ym || d.date) === Y);
    if (orphan.length) {
      XLSX.utils.book_append_sheet(wb, U.xlSheet({
        title: 'OF ที่ไม่มีใบจ่ายในทะเบียน', note: 'ตั้ง OF ใน Mango แล้ว แต่หาใบเบิกที่ยอด/ฝ่ายตรงกันไม่เจอ',
        columns: [
          { key: 'docNo', label: 'เลข OF', width: 15 }, { key: 'date', label: 'วันที่', width: 12 }, { key: 'emp', label: 'ผู้ตั้ง', width: 24 },
          { key: 'dept', label: 'ฝ่าย', width: 8 }, { key: 'amt', label: 'ยอดที่ยังไม่ผูก', type: 'number', width: 13 },
          { key: 'pv', label: 'จ่ายคืน (PV)', width: 22 },
        ],
        rows: orphan.map(d => ({ docNo: d.docNo, date: pcD(d.date), emp: d.emp, dept: d.depts.join(', '), amt: d.freeAmt,
          pv: d.pvNo ? d.pvNo + ' · ' + pcD(d.payDate) : '' })),
      }), 'OF ไม่มีใบจ่าย');
    }
    const ex = {};
    wb.SheetNames.forEach(n => { ex[n] = { freeze: 3, tab: '1A4490' }; });
    U.xlsxDownload(wb, 'ทะเบียนคุมเงินสดย่อย ' + Y + '.xlsx', ex);
    if (window.WTPData && WTPData.logEvent) WTPData.logEvent('export', '', 'ส่งออกทะเบียนคุมเงินสดย่อย ปี ' + Y + ' (' + reg.length + ' แถว)');
  }
  function pcDeptSummary(b, Y) {
    const map = {};
    b.pays.filter(p => pcYear(p.payDate) === Y && p.status !== 'ignored').forEach(p => {
      const k = p.deptCode || 'ไม่ระบุ';
      const g = map[k] || (map[k] = { dept: k, n: 0, total: 0, back: 0, wait: 0, none: 0 });
      g.n++; g.total = pcR2(g.total + p.net);
      if (p.state === 'in') g.back = pcR2(g.back + p.net);
      else if (PC_WAIT[p.state]) g.wait = pcR2(g.wait + p.net);
      else g.none = pcR2(g.none + p.net);
    });
    return Object.values(map).sort((a, b2) => b2.total - a.total);
  }

  /* ── UI ชิ้นเล็ก ──────────────────────────────────────────────── */
  function PcTag({ tone, children, title, onClick }) {
    const S = {
      ok: ['var(--good-bg)', 'var(--good)'], warn: ['var(--warn-bg)', 'var(--warn)'], bad: ['var(--bad-bg)', 'var(--bad)'],
      info: ['color-mix(in srgb, var(--brand-500) 13%, white)', 'var(--brand-700)'], mute: ['var(--ink-100)', 'var(--ink-600)'],
    }[tone] || ['var(--ink-100)', 'var(--ink-600)'];
    return (
      <span title={title} onClick={onClick} style={{ background: S[0], color: S[1], fontSize: 11, fontWeight: 700,
        borderRadius: 999, padding: '2px 9px', whiteSpace: 'nowrap', display: 'inline-block', cursor: onClick ? 'pointer' : undefined }}>
        {children}
      </span>
    );
  }
  const PC_TH_BG = 'color-mix(in srgb, var(--brand-500) 12%, white)';
  const pcTh = (align, w) => ({
    padding: '6px 8px', fontSize: 10.5, fontWeight: 700, color: 'var(--ink-600)', letterSpacing: .2,
    background: PC_TH_BG, borderBottom: '1px solid var(--line)', whiteSpace: 'nowrap',
    textAlign: align || 'left', position: 'sticky', top: 0, zIndex: 1, width: w,
  });
  const pcTd = (align, extra) => Object.assign({
    padding: '5px 8px', fontSize: 12, borderTop: '1px solid var(--ink-100)', textAlign: align || 'left', verticalAlign: 'middle',
  }, align === 'right' ? { fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' } : null, extra || {});
  const pcLinkBtn = { border: 'none', background: 'none', padding: 0, cursor: 'pointer', color: 'var(--brand-700)', fontFamily: 'inherit', fontSize: 'inherit', fontWeight: 600 };
  const pcChip = (on) => ({ border: 'none', cursor: 'pointer', borderRadius: 999, padding: '4px 11px', fontSize: 11.5, fontWeight: 600, fontFamily: 'inherit',
    background: on ? 'var(--brand-500)' : 'var(--ink-100)', color: on ? '#fff' : 'var(--ink-700)' });
  const pcLbl = { fontSize: 11.5, fontWeight: 700, color: 'var(--ink-600)', display: 'block', marginBottom: 3 };

  function PcTile({ label, value, sub, tone, active, onClick }) {
    const col = { bad: 'var(--bad)', warn: 'var(--warn)', ok: 'var(--good)', info: 'var(--brand-700)' }[tone] || 'var(--ink-800)';
    return (
      <div onClick={onClick} className="card" style={{ flex: '1 1 190px', padding: '11px 14px', cursor: onClick ? 'pointer' : 'default',
        borderLeft: '4px solid ' + col, outline: active ? '2px solid ' + col : 'none', outlineOffset: -1 }}>
        <div style={{ fontSize: 11.5, fontWeight: 700, color: 'var(--ink-600)' }}>{label}</div>
        <div style={{ fontSize: 21, fontWeight: 800, color: col, fontVariantNumeric: 'tabular-nums', marginTop: 1 }}>{value}</div>
        {sub ? <div style={{ fontSize: 11.5, color: 'var(--ink-500)', marginTop: 1 }}>{sub}</div> : null}
      </div>
    );
  }

  /* สถานะของใบจ่าย (ช่องสถานะในทะเบียน) */
  function PcPayState({ p, onRound }) {
    const s = PC_STATE[p.state];
    let sub = '';
    if (p.state === 'in') sub = 'รอบ ' + pcD(p.round.payDate);
    else if (p.state === 'none') sub = p.age != null ? 'จ่ายไป ' + p.age + ' วัน' : '';
    else if (p.state === 'ofpaid') {
      const pv = (p.ofLines.find(l => pcTrim(l.pvNo)) || {}).pvNo || '';
      sub = 'บัญชีจ่าย' + (p.ofPayDate ? ' ' + pcD(p.ofPayDate) : 'แล้ว') + (pv ? ' · ' + pv : '');
    }
    else if (p.state === 'of') sub = p.matchBy === 'auto' ? 'ระบบจับให้' : '';
    return (
      <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
        <PcTag tone={s.tone} onClick={p.state === 'in' && onRound ? () => onRound(p.round.id) : undefined}>{s.label}</PcTag>
        {sub && <span style={{ fontSize: 11, color: 'var(--ink-500)' }}>{sub}</span>}
      </span>
    );
  }
  /* ช่อง OF ของใบจ่าย */
  function PcOfCell({ p, onClick }) {
    const typed = pcOfNorm(p.ofNo);
    const deptOff = p.ofLines.length && p.deptCode && p.ofDepts.some(d => d !== pcDept(p.deptCode));
    const body = p.ofLines.length ? (
      <span>
        {p.ofDocs.join(', ')}
        {p.ofLines.length > 1 && <span style={{ color: 'var(--ink-500)', fontWeight: 400 }}> · {p.ofLines.length} บรรทัด</span>}
        {!p.ofDiffOk && <span style={{ color: 'var(--bad)', fontWeight: 700 }}> · ยอด OF {pcMoney(p.ofSum)} (ต่าง {pcMoney(p.ofDiff)})</span>}
        {deptOff ? <span style={{ color: 'var(--warn)', fontWeight: 600 }}> · OF ลงฝ่าย {p.ofDepts.join('/')}</span> : null}
      </span>
    ) : typed ? <span>{typed} <span style={{ color: 'var(--ink-500)', fontWeight: 400 }}>(จดไว้)</span></span>
      : <span style={{ color: 'var(--ink-400)', fontWeight: 400 }}>{onClick ? '＋ ผูก OF' : '—'}</span>;
    return onClick ? <button style={pcLinkBtn} onClick={onClick} title="ผูก / เปลี่ยนบรรทัด OF ของใบนี้">{body}</button> : body;
  }

  /* ── หน้าต่างบันทึก / แก้ไข ใบจ่ายออก (เพิ่มได้หลายใบในครั้งเดียว) ── */
  function PcPayModal({ edit, b, rounds, deptOptions, whoOptions, whoDept, canDelete, onClose, onSave, onDelete }) {
    const isEdit = !!edit;
    const today = pcToday();
    const mkRow = (prev) => {
      const d = prev.length ? prev[prev.length - 1].payDate : today;
      return { _k: Math.random().toString(36).slice(2), payDate: d, checkDate: '', docNo: pcNextDocNo(b.pays, d, prev), docAuto: true,
        requester: '', deptCode: '', amount: '', description: '', ofNo: '' };
    };
    const [rows, setRows] = useState(() => isEdit
      ? [Object.assign({}, edit, { _k: 'e', amount: String(pcR2(edit.amount)), docAuto: false })]
      : [mkRow([])]);
    const [roundId, setRoundId] = useState(isEdit ? (edit.roundId || '') : '');
    const [ignored, setIgnored] = useState(isEdit && edit.status === 'ignored');
    const set = (k, field, v) => setRows(prev => prev.map(r => {
      if (r._k !== k) return r;
      const o = Object.assign({}, r, { [field]: v });
      if (field === 'docNo') o.docAuto = false;
      if (field === 'requester' && !pcTrim(r.deptCode) && whoDept[pcTrim(v)]) o.deptCode = whoDept[pcTrim(v)];
      if (field === 'deptCode') o.deptCode = String(v).toUpperCase();
      if ((field === 'payDate' || field === 'checkDate') && r.docAuto) {
        const others = prev.filter(x => x._k !== k);
        o.docNo = pcNextDocNo(b.pays.filter(p => !isEdit || p.id !== edit.id), o.checkDate || o.payDate, others);
      }
      return o;
    }));
    const addRow = () => setRows(prev => prev.concat([mkRow(prev)]));
    const delRow = (k) => setRows(prev => (prev.length > 1 ? prev.filter(r => r._k !== k) : prev));
    const problems = rows.map(r => {
      if (!r.payDate) return 'ใส่วันที่จ่าย';
      if (!(pcNum(r.amount) > 0)) return 'ใส่จำนวนเงิน';
      if (!pcTrim(r.requester) && !pcTrim(r.description)) return 'ใส่ผู้เบิก';
      return '';
    });
    const dupOf = (r) => {
      const y = pcYear(r.checkDate || r.payDate), dn = pcTrim(r.docNo);
      if (!dn) return null;
      return b.pays.find(p => (!isEdit || p.id !== edit.id) && pcTrim(p.docNo) === dn && pcYear(p.checkDate || p.payDate) === y) || null;
    };
    const total = pcR2(rows.reduce((s, r) => s + pcNum(r.amount), 0));
    const ok = problems.every(x => !x);
    const inp = { fontSize: 12.5, padding: '5px 7px', width: '100%' };
    return (
      <Modal open maxWidth={isEdit ? 640 : 1020} onClose={onClose}
        title={isEdit ? '✏️ แก้ไขใบจ่ายออก ' + (edit.docNo || '') : '💸 บันทึกจ่ายออก (โอนเงินให้ผู้เบิก)'}
        footer={<>
          {isEdit && canDelete && (
            <button className="btn btn-ghost" style={{ color: 'var(--bad)', marginRight: 'auto' }}
              onClick={() => { if (confirm('ลบใบ ' + (edit.docNo || '') + ' ' + pcMoney(edit.amount) + ' บาท ออกจากทะเบียน?')) onDelete(edit); }}>
              <Icon name="trash" size={14} /> ลบใบนี้
            </button>
          )}
          {!isEdit && <span style={{ marginRight: 'auto', fontSize: 12, color: 'var(--ink-600)' }}>{rows.length} ใบ · รวม {pcMoney(total)} บาท</span>}
          <button className="btn btn-ghost" onClick={onClose}>ยกเลิก</button>
          <button className="btn btn-primary" disabled={!ok} title={problems.find(Boolean) || ''}
            onClick={() => onSave(rows, { roundId, ignored })}>บันทึก{isEdit ? '' : ' ' + rows.length + ' ใบ'}</button>
        </>}>
        <datalist id="pc-who">{whoOptions.map(w => <option key={w} value={w} />)}</datalist>
        <datalist id="pc-dept">{deptOptions.map(d => <option key={d} value={d} />)}</datalist>
        {!isEdit ? (
          <>
            <div style={{ fontSize: 12, color: 'var(--ink-500)', marginBottom: 8 }}>
              เลขที่เอกสารระบบเสนอเลขถัดไปของเดือนให้ (MM-NN) · พิมพ์ชื่อผู้เบิกแล้วฝ่ายเติมให้จากครั้งก่อน · ช่องสุดท้ายกด Enter = เพิ่มแถว
            </div>
            <div style={{ overflowX: 'auto' }}>
              <table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 880 }}>
                <thead><tr>
                  <th style={pcTh('left', 128)}>วันที่จ่าย</th><th style={pcTh('left', 84)}>เลขที่</th>
                  <th style={pcTh('left', 130)}>ผู้เบิก</th><th style={pcTh('left', 74)}>ฝ่าย</th>
                  <th style={pcTh('right', 108)}>จำนวนเงิน</th><th style={pcTh('left')}>รายการ</th>
                  <th style={pcTh('left', 120)}>เลข OF (ถ้ามี)</th><th style={pcTh('center', 30)}></th>
                </tr></thead>
                <tbody>
                  {rows.map((r, i) => {
                    const dup = dupOf(r);
                    return (
                      <tr key={r._k}>
                        <td style={pcTd()}><input className="input" type="date" style={inp} value={r.payDate || ''} onChange={e => set(r._k, 'payDate', e.target.value)} /></td>
                        <td style={pcTd()}>
                          <input className="input" style={Object.assign({}, inp, dup ? { borderColor: 'var(--warn)' } : null)} value={r.docNo || ''}
                            title={dup ? 'เลขซ้ำกับใบ ' + dup.docNo + ' ของ ' + (dup.requester || '') + ' (' + pcD(dup.payDate) + ') — บันทึกได้' : ''}
                            onChange={e => set(r._k, 'docNo', e.target.value)} />
                        </td>
                        <td style={pcTd()}><input className="input" list="pc-who" style={inp} value={r.requester || ''} autoFocus={i === rows.length - 1 && i > 0}
                          onChange={e => set(r._k, 'requester', e.target.value)} /></td>
                        <td style={pcTd()}><input className="input" list="pc-dept" style={inp} value={r.deptCode || ''} onChange={e => set(r._k, 'deptCode', e.target.value)} /></td>
                        <td style={pcTd()}><input className="input" type="number" step="0.01" min="0" style={Object.assign({}, inp, { textAlign: 'right' })}
                          value={r.amount} onChange={e => set(r._k, 'amount', e.target.value)} /></td>
                        <td style={pcTd()}><input className="input" style={inp} value={r.description || ''} placeholder="ค่าอะไร (ไม่บังคับ)" onChange={e => set(r._k, 'description', e.target.value)} /></td>
                        <td style={pcTd()}><input className="input" style={inp} value={r.ofNo || ''} placeholder="OF26…"
                          onChange={e => set(r._k, 'ofNo', e.target.value)}
                          onKeyDown={e => { if (e.key === 'Enter' && i === rows.length - 1) { e.preventDefault(); addRow(); } }} /></td>
                        <td style={pcTd('center')}>
                          {rows.length > 1 && <button className="btn btn-ghost" style={{ padding: '2px 6px', color: 'var(--bad)' }} onClick={() => delRow(r._k)} title="เอาแถวนี้ออก">✕</button>}
                        </td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
            <button className="btn btn-ghost" style={{ marginTop: 8 }} onClick={addRow}><Icon name="plus" size={14} /> เพิ่มแถว</button>
          </>
        ) : (() => {
          const r = rows[0], dup = dupOf(r);
          return (
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px 14px' }}>
              <label><span style={pcLbl}>วันที่จ่ายเงิน</span><input className="input" type="date" style={inp} value={r.payDate || ''} onChange={e => set(r._k, 'payDate', e.target.value)} /></label>
              <label><span style={pcLbl}>วันที่ตรวจเอกสาร</span><input className="input" type="date" style={inp} value={r.checkDate || ''} onChange={e => set(r._k, 'checkDate', e.target.value)} /></label>
              <label><span style={pcLbl}>เลขที่เอกสาร {dup && <span style={{ color: 'var(--warn)' }}>· ซ้ำกับใบของ {dup.requester || '—'}</span>}</span>
                <input className="input" style={inp} value={r.docNo || ''} onChange={e => set(r._k, 'docNo', e.target.value)} /></label>
              <label><span style={pcLbl}>จำนวนเงิน</span><input className="input" type="number" step="0.01" min="0" style={Object.assign({}, inp, { textAlign: 'right' })} value={r.amount} onChange={e => set(r._k, 'amount', e.target.value)} /></label>
              <label><span style={pcLbl}>ผู้เบิก</span><input className="input" list="pc-who" style={inp} value={r.requester || ''} onChange={e => set(r._k, 'requester', e.target.value)} /></label>
              <label><span style={pcLbl}>ฝ่าย</span><input className="input" list="pc-dept" style={inp} value={r.deptCode || ''} onChange={e => set(r._k, 'deptCode', e.target.value)} /></label>
              <label style={{ gridColumn: '1 / -1' }}><span style={pcLbl}>รายการ</span><input className="input" style={inp} value={r.description || ''} onChange={e => set(r._k, 'description', e.target.value)} /></label>
              <label><span style={pcLbl}>เลข OF ที่จดไว้</span><input className="input" style={inp} value={r.ofNo || ''} placeholder="OF26…" onChange={e => set(r._k, 'ofNo', e.target.value)} /></label>
              <label><span style={pcLbl}>เงินเข้ารอบ</span>
                <select className="select input" style={inp} value={roundId} onChange={e => setRoundId(e.target.value)}>
                  <option value="">— ยังไม่ได้เงินคืน —</option>
                  {rounds.slice().sort((a, c) => (a.payDate < c.payDate ? 1 : -1)).map(x => (
                    <option key={x.id} value={x.id}>{pcD(x.payDate)} · {pcMoney(x.amount)}{x.description && !/รับเงินสดย่อย/.test(x.description) ? ' · ' + x.description : ''}</option>
                  ))}
                </select></label>
              <label><span style={pcLbl}>ไฟล์สแกน</span><input className="input" style={inp} value={r.scanNote || ''} placeholder="เช่น ได้ไฟล์สแกนแล้ว 08.01.2026" onChange={e => set(r._k, 'scanNote', e.target.value)} /></label>
              <label><span style={pcLbl}>หมายเหตุ</span><input className="input" style={inp} value={r.note || ''} onChange={e => set(r._k, 'note', e.target.value)} /></label>
              <label style={{ gridColumn: '1 / -1', display: 'flex', alignItems: 'center', gap: 7, fontSize: 12.5, cursor: 'pointer' }}>
                <input type="checkbox" checked={ignored} onChange={e => setIgnored(e.target.checked)} />
                ใบนี้ไม่ต้องตั้ง OF (ไม่นับเป็นค้างรับคืน — ระบุเหตุผลในหมายเหตุ)
              </label>
            </div>
          );
        })()}
      </Modal>
    );
  }

  /* ── หน้าต่างบันทึกเงินเข้า / รับอื่น — เงินเข้ารอบนี้เป็นของใบไหนบ้าง ── */
  function PcInModal({ edit, preset, b, canDelete, onClose, onSave, onDelete }) {
    const isEdit = !!edit;
    const presetIds = preset || [];
    const presetSum = pcR2(presetIds.reduce((s, id) => s + ((b.payById[id] || {}).net || 0), 0));
    const [kind, setKind] = useState(isEdit ? edit.kind : 'in');
    const [date, setDate] = useState(isEdit ? (edit.payDate || '') : pcToday());
    const [amount, setAmount] = useState(isEdit ? String(pcR2(edit.amount)) : (presetIds.length ? String(presetSum) : ''));
    const [desc, setDesc] = useState(isEdit ? (edit.description || '') : '');
    const [refDoc, setRefDoc] = useState(isEdit ? (edit.refDoc || '') : '');
    const [note, setNote] = useState(isEdit ? (edit.note || '') : '');
    const [picked, setPicked] = useState(() => {
      const s = {};
      if (isEdit) b.pays.forEach(p => { if (p.roundId === edit.id) s[p.id] = true; });
      presetIds.forEach(id => { s[id] = true; });
      return s;
    });
    const [touched, setTouched] = useState(isEdit || presetIds.length > 0);
    const [why, setWhy] = useState('');
    const [showLater, setShowLater] = useState(false);
    const cands = useMemo(() => b.pays.filter(p => p.status !== 'ignored'
      && (!p.round || (isEdit && p.roundId === edit.id))).sort(pcByDate), [b]);
    const shown = cands.filter(p => picked[p.id] || showLater || !date || !p.payDate || p.payDate <= date);
    const hidden = cands.length - shown.length;
    useEffect(() => {
      if (kind !== 'in' || touched) return;
      const pool = cands.filter(p => !date || !p.payDate || p.payDate <= date);
      const pr = pcPropose(amount, date, pool);
      const s = {};
      if (pr) pr.ids.forEach(id => { s[id] = true; });
      setPicked(s);
      setWhy(pr ? pr.why : (pcNum(amount) > 0 ? 'หาชุดใบที่รวมได้เท่ายอดนี้ไม่เจอ — ติ๊กเองได้' : ''));
    }, [amount, date, kind, touched]);
    const toggle = (id) => { setTouched(true); setWhy(''); setPicked(prev => { const n = Object.assign({}, prev); if (n[id]) delete n[id]; else n[id] = true; return n; }); };
    const auto = () => { setTouched(false); };
    const pickedList = cands.filter(p => picked[p.id]);
    const pickedSum = pcR2(pickedList.reduce((s, p) => s + p.net, 0));
    const diff = pcR2(pcNum(amount) - pickedSum);
    const refOptions = b.pays.slice().sort((a, c) => (a.payDate < c.payDate ? 1 : -1)).slice(0, 120);
    const ok = date && pcNum(amount) > 0 && (kind !== 'refund' || pcTrim(refDoc));
    const kinds = ['in', 'other', 'refund', 'open'];
    return (
      <Modal open maxWidth={kind === 'in' ? 900 : 560} onClose={onClose}
        title={isEdit ? '✏️ แก้ไข' + PC_KINDS[edit.kind].label + ' ' + pcD(edit.payDate) : '💰 บันทึกเงินเข้าวงเงินสดย่อย'}
        footer={<>
          {isEdit && canDelete && (
            <button className="btn btn-ghost" style={{ color: 'var(--bad)', marginRight: 'auto' }}
              onClick={() => { if (confirm('ลบแถวเงินเข้า ' + pcD(edit.payDate) + ' ' + pcMoney(edit.amount) + ' บาท?' + (kind === 'in' ? '\nใบที่ผูกกับรอบนี้จะกลับเป็น "ยังไม่ได้เงินคืน"' : ''))) onDelete(edit); }}>
              <Icon name="trash" size={14} /> ลบ
            </button>
          )}
          {kind === 'in' && (
            <span style={{ marginRight: isEdit && canDelete ? 0 : 'auto', fontSize: 12.5, fontWeight: 700,
              color: Math.abs(diff) < 0.01 ? 'var(--good)' : 'var(--warn)' }}>
              ติ๊ก {pickedList.length} ใบ = {pcMoney(pickedSum)}
              {Math.abs(diff) < 0.01 ? ' ✓ ตรงกับยอดเงินเข้า' : ' · ต่างจากยอดเงินเข้า ' + pcMoney(diff)}
            </span>
          )}
          <button className="btn btn-ghost" onClick={onClose}>ยกเลิก</button>
          <button className="btn btn-primary" disabled={!ok}
            onClick={() => onSave({ kind, payDate: date, amount: pcR2(amount), description: desc, refDoc, note },
              kind === 'in' ? pickedList.map(p => p.id) : null)}>บันทึก</button>
        </>}>
        {!isEdit && (
          <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 12 }}>
            {kinds.map(k => <button key={k} style={pcChip(kind === k)} onClick={() => setKind(k)}>{PC_KINDS[k].label}</button>)}
          </div>
        )}
        <div style={{ display: 'grid', gridTemplateColumns: '150px 160px 1fr', gap: '10px 14px', alignItems: 'end' }}>
          <label><span style={pcLbl}>วันที่เงินเข้า</span><input className="input" type="date" style={{ width: '100%' }} value={date} onChange={e => setDate(e.target.value)} /></label>
          <label><span style={pcLbl}>จำนวนเงิน</span><input className="input" type="number" step="0.01" min="0" style={{ width: '100%', textAlign: 'right' }}
            value={amount} autoFocus={!isEdit} onChange={e => setAmount(e.target.value)} /></label>
          <label><span style={pcLbl}>รายการ</span><input className="input" style={{ width: '100%' }} value={desc}
            placeholder={kind === 'in' ? 'รับเงินสดย่อย' : kind === 'other' ? 'ดอกเบี้ยรับ' : kind === 'open' ? 'ตั้งวงเงินสดย่อย / ยอดยกมา' : 'รับคืนเงินทอน'}
            onChange={e => setDesc(e.target.value)} /></label>
          {kind === 'refund' && (
            <label style={{ gridColumn: '1 / -1' }}><span style={pcLbl}>เงินทอนของใบเลขที่</span>
              <select className="select input" style={{ width: '100%' }} value={refDoc} onChange={e => setRefDoc(e.target.value)}>
                <option value="">— เลือกใบ —</option>
                {refOptions.map(p => <option key={p.id} value={p.docNo}>{p.docNo} · {p.requester} · {pcMoney(p.amount)} · {pcD(p.payDate)}</option>)}
              </select></label>
          )}
          <label style={{ gridColumn: '1 / -1' }}><span style={pcLbl}>หมายเหตุ</span><input className="input" style={{ width: '100%' }} value={note} onChange={e => setNote(e.target.value)} /></label>
        </div>
        {kind === 'in' && (
          <div style={{ marginTop: 14 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 6, flexWrap: 'wrap' }}>
              <b style={{ fontSize: 13 }}>เงินก้อนนี้เป็นของใบไหนบ้าง</b>
              <button className="btn btn-ghost" style={{ padding: '3px 9px', fontSize: 12 }} onClick={auto} title="ให้ระบบเลือกใบที่รวมได้เท่ายอดเงินเข้า">🪄 เลือกให้อัตโนมัติ</button>
              {why && <span style={{ fontSize: 11.5, color: 'var(--ink-500)' }}>{why}</span>}
              {hidden > 0 && (
                <label style={{ marginLeft: 'auto', fontSize: 11.5, color: 'var(--ink-500)', display: 'flex', gap: 5, alignItems: 'center', cursor: 'pointer' }}>
                  <input type="checkbox" checked={showLater} onChange={e => setShowLater(e.target.checked)} />
                  แสดงใบที่จ่ายหลังวันเงินเข้า ({hidden})
                </label>
              )}
            </div>
            <div style={{ border: '1px solid var(--line)', borderRadius: 10, maxHeight: 330, overflow: 'auto' }}>
              <table style={{ width: '100%', borderCollapse: 'collapse' }}>
                <thead><tr>
                  <th style={pcTh('center', 30)}></th><th style={pcTh('left', 90)}>วันที่จ่าย</th><th style={pcTh('left', 70)}>เลขที่</th>
                  <th style={pcTh('left')}>ผู้เบิก</th><th style={pcTh('left', 56)}>ฝ่าย</th><th style={pcTh('right', 96)}>ยอด</th>
                  <th style={pcTh('left')}>OF / สถานะ</th>
                </tr></thead>
                <tbody>
                  {shown.length === 0 && <tr><td colSpan={7} style={pcTd('center', { padding: 18, color: 'var(--ink-500)' })}>ไม่มีใบที่ค้างรับคืน</td></tr>}
                  {shown.map(p => (
                    <tr key={p.id} onClick={() => toggle(p.id)} style={{ cursor: 'pointer', background: picked[p.id] ? 'color-mix(in srgb, var(--good) 9%, white)' : undefined }}>
                      <td style={pcTd('center')}><input type="checkbox" checked={!!picked[p.id]} readOnly /></td>
                      <td style={pcTd()}>{pcD(p.payDate)}</td><td style={pcTd()}>{p.docNo}</td><td style={pcTd()}>{p.requester}</td>
                      <td style={pcTd()}>{p.deptCode}</td>
                      <td style={pcTd('right')}>{pcMoney(p.net)}{p.refund ? <div style={{ fontSize: 10.5, color: 'var(--ink-500)' }}>หักเงินทอน {pcMoney(p.refund)}</div> : null}</td>
                      <td style={pcTd()}>
                        <span style={{ fontSize: 11.5 }}>{p.ofDocs.join(', ') || pcOfNorm(p.ofNo) || ''}</span>{' '}
                        <PcTag tone={PC_STATE[p.state].tone}>{p.state === 'ofpaid' ? 'บัญชีจ่ายแล้ว' + (p.ofPayDate ? ' ' + pcDs(p.ofPayDate) : '') : PC_STATE[p.state].label}</PcTag>
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </div>
        )}
      </Modal>
    );
  }

  /* ── หน้าต่างผูกใบจ่ายกับบรรทัด OF ── */
  function PcLinkModal({ pay, b, onClose, onSave }) {
    const [picked, setPicked] = useState(() => { const s = {}; pay.ofLines.forEach(l => { s[l.id] = true; }); return s; });
    const [onlyDept, setOnlyDept] = useState(!!pay.deptCode);
    const [q, setQ] = useState('');
    const [ofNo, setOfNo] = useState(pay.ofNo || '');
    const [ignored, setIgnored] = useState(pay.status === 'ignored');
    const pd = pcDept(pay.deptCode);
    const qq = pcTrim(q).toLowerCase();
    const lines = b.ofs.filter(l => l.status !== 'ignored').filter(l => {
      if (picked[l.id]) return true;
      if (onlyDept && pd && pcDept(l.deptCode) !== pd) return false;
      if (qq && (l.docNo + ' ' + l.empName + ' ' + l.itemName + ' ' + l.vendor + ' ' + l.amount).toLowerCase().indexOf(qq) < 0) return false;
      return true;
    }).map(l => ({ l, owner: b.lineOwner[l.id] && b.lineOwner[l.id] !== pay.id ? b.payById[b.lineOwner[l.id]] : null,
      dd: (l.refDate && pay.payDate) ? Math.abs(pcDays(pay.payDate, l.refDate)) : 999 }));
    const docs = {};
    lines.forEach(x => { (docs[x.l.docNo] = docs[x.l.docNo] || []).push(x); });
    const docList = Object.keys(docs).map(dn => ({ dn, items: docs[dn], dd: Math.min.apply(null, docs[dn].map(x => x.dd)),
      anyPicked: docs[dn].some(x => picked[x.l.id]), free: docs[dn].some(x => !x.owner) }))
      .sort((a, c) => (c.anyPicked - a.anyPicked) || (c.free - a.free) || (a.dd - c.dd)).slice(0, 60);
    const pickedLines = b.ofs.filter(l => picked[l.id]);
    const sum = pcR2(pickedLines.reduce((s, l) => s + pcNum(l.amount), 0));
    const diff = pcR2(sum - pay.net);
    const toggle = (id) => setPicked(prev => { const n = Object.assign({}, prev); if (n[id]) delete n[id]; else n[id] = true; return n; });
    const toggleDoc = (items) => setPicked(prev => {
      const n = Object.assign({}, prev);
      const all = items.every(x => n[x.l.id]);
      items.forEach(x => { if (all) delete n[x.l.id]; else n[x.l.id] = true; });
      return n;
    });
    return (
      <Modal open maxWidth={900} onClose={onClose} title={'🔗 ผูก OF ของใบ ' + (pay.docNo || '') + ' · ' + (pay.requester || '') + ' · ' + pcMoney(pay.net) + ' บาท'}
        footer={<>
          <span style={{ marginRight: 'auto', fontSize: 12.5, fontWeight: 700, color: !pickedLines.length ? 'var(--ink-500)' : Math.abs(diff) < 0.01 ? 'var(--good)' : 'var(--warn)' }}>
            {pickedLines.length ? 'เลือก ' + pickedLines.length + ' บรรทัด = ' + pcMoney(sum) + (Math.abs(diff) < 0.01 ? ' ✓ ตรงกับยอดใบเบิก' : ' · ต่าง ' + pcMoney(diff)) : 'ยังไม่ได้เลือกบรรทัด OF'}
          </span>
          {pcIds(pay.ofIds).length > 0 && (
            <button className="btn btn-ghost" onClick={() => onSave({ ofIds: [], ofNo, ignored, clear: true })} title="เลิกผูกเอง ให้ระบบจับคู่ให้">ให้ระบบจับเอง</button>
          )}
          <button className="btn btn-ghost" onClick={onClose}>ยกเลิก</button>
          <button className="btn btn-primary" onClick={() => onSave({ ofIds: pickedLines.map(l => l.id), ofNo, ignored })}>บันทึก</button>
        </>}>
        <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'center', marginBottom: 10, fontSize: 12 }}>
          <span style={{ color: 'var(--ink-600)' }}>จ่าย {pcD(pay.payDate)} · ฝ่าย <b>{pay.deptCode || '—'}</b></span>
          <label style={{ display: 'flex', alignItems: 'center', gap: 5, cursor: 'pointer' }}>
            <input type="checkbox" checked={onlyDept} onChange={e => setOnlyDept(e.target.checked)} disabled={!pd} /> เฉพาะ OF ฝ่าย {pd || '—'}
          </label>
          <input className="input" placeholder="ค้นหา เลข OF / ชื่อ / ยอด" value={q} onChange={e => setQ(e.target.value)} style={{ width: 210, fontSize: 12 }} />
          <label style={{ display: 'flex', alignItems: 'center', gap: 5 }}>เลข OF ที่จดไว้
            <input className="input" value={ofNo} onChange={e => setOfNo(e.target.value)} placeholder="OF26…" style={{ width: 130, fontSize: 12 }} /></label>
          <label style={{ display: 'flex', alignItems: 'center', gap: 5, cursor: 'pointer', marginLeft: 'auto' }}>
            <input type="checkbox" checked={ignored} onChange={e => setIgnored(e.target.checked)} /> ใบนี้ไม่ต้องตั้ง OF
          </label>
        </div>
        {b.ofs.length === 0 ? (
          <div style={{ padding: 18, textAlign: 'center', color: 'var(--ink-500)', fontSize: 12.5, background: 'var(--ink-50)', borderRadius: 10 }}>
            ยังไม่ได้นำเข้า OF จาก Mango — จดเลข OF ไว้ในช่องด้านบนก่อนได้ (นับเป็น "ตั้ง OF แล้ว")
          </div>
        ) : (
          <div style={{ border: '1px solid var(--line)', borderRadius: 10, maxHeight: 380, overflow: 'auto' }}>
            <table style={{ width: '100%', borderCollapse: 'collapse' }}>
              <thead><tr>
                <th style={pcTh('center', 30)}></th><th style={pcTh('left', 110)}>เลข OF</th><th style={pcTh('left', 84)}>วันที่</th>
                <th style={pcTh('left')}>ผู้ตั้ง · รายการ</th><th style={pcTh('left', 50)}>ฝ่าย</th><th style={pcTh('right', 92)}>ยอด</th>
                <th style={pcTh('left', 120)}>ผูกกับใบ</th>
              </tr></thead>
              <tbody>
                {docList.length === 0 && <tr><td colSpan={7} style={pcTd('center', { padding: 18, color: 'var(--ink-500)' })}>ไม่พบบรรทัด OF ตามเงื่อนไข</td></tr>}
                {docList.map(d => (
                  <React.Fragment key={d.dn}>
                    {d.items.length > 1 && (
                      <tr style={{ background: 'var(--ink-50)' }}>
                        <td style={pcTd('center')}><input type="checkbox" checked={d.items.every(x => picked[x.l.id])} onChange={() => toggleDoc(d.items)} /></td>
                        <td style={pcTd('left', { fontWeight: 700 })} colSpan={4}>{d.dn} <span style={{ fontWeight: 400, color: 'var(--ink-500)' }}>· ทั้งใบ {d.items.length} บรรทัด</span></td>
                        <td style={pcTd('right', { fontWeight: 700 })}>{pcMoney(d.items.reduce((s, x) => s + pcNum(x.l.amount), 0))}</td><td style={pcTd()}></td>
                      </tr>
                    )}
                    {d.items.map(x => (
                      <tr key={x.l.id} onClick={() => toggle(x.l.id)} style={{ cursor: 'pointer', background: picked[x.l.id] ? 'color-mix(in srgb, var(--good) 9%, white)' : undefined }}>
                        <td style={pcTd('center')}><input type="checkbox" checked={!!picked[x.l.id]} readOnly /></td>
                        <td style={pcTd()}>{d.items.length > 1 ? '' : x.l.docNo}</td>
                        <td style={pcTd()}>{pcD(x.l.refDate)}</td>
                        <td style={pcTd('left', { fontSize: 11.5 })}>{x.l.empName}{x.l.itemName ? ' · ' + x.l.itemName : ''}{x.l.vendor ? <span style={{ color: 'var(--ink-500)' }}> · {x.l.vendor}</span> : null}</td>
                        <td style={pcTd()}>{x.l.deptCode}</td>
                        <td style={pcTd('right')}>{pcMoney(x.l.amount)}</td>
                        <td style={pcTd('left', { fontSize: 11 })}>{x.owner ? <span style={{ color: 'var(--warn)' }}>ใบ {x.owner.docNo} (จะย้ายมา)</span> : ''}</td>
                      </tr>
                    ))}
                  </React.Fragment>
                ))}
              </tbody>
            </table>
          </div>
        )}
      </Modal>
    );
  }

  /* ── หน้าต่างนำเข้า OF จาก Mango (รายงานเดียวกับ Fleet Card) ── */
  const PC_PAYEE_HINT = /เงินสดย่อย|petty|ปาฏิมา|ชี้รัตน์/i;
  const PC_PAYEE_NOT = /FLEET|EXECUTIVE/i;
  const pcPayeeKey = (s) => pcTrim(s).replace(/\s+/g, ' ').slice(0, 80) || '(ไม่ระบุผู้รับเงิน)';
  function PcOfImportModal({ existing, onClose, onCommit }) {
    const [items, setItems] = useState([]);
    const [drag, setDrag] = useState(false);
    const [pick, setPick] = useState(null);           // { payee: true } — null = ยังไม่แตะ ใช้ค่าเริ่มต้น
    const inputRef = useRef(null);
    const U = FCU();
    const addFiles = (fileList) => {
      Array.from(fileList || []).forEach(file => {
        const key = file.name + '|' + file.size + '|' + Math.random().toString(36).slice(2, 7);
        setItems(prev => prev.concat([{ key, name: file.name, status: 'reading', rows: [] }]));
        U.readWorkbook(file).then(wb => {
          const sheet = U.pickMangoSheet(wb.sheetNames, wb.sheets);
          const aoa = wb.sheets[sheet] || [];
          const parsed = U.parseMangoAoa(aoa);
          if (parsed.kind === 'header') {
            // หัวใบ (rpt130) ไม่มี acctname — ผู้รับเงินอยู่ที่ cust_name
            const hr = U.guessHeaderRow(aoa);
            const H = (aoa[hr] || []).map(c => pcTrim(c).toLowerCase());
            const ci = H.indexOf('cust_name'), di = H.indexOf('docno');
            if (ci >= 0 && di >= 0) {
              const map = {};
              for (let i = hr + 1; i < aoa.length; i++) { const r = aoa[i] || []; if (r[di]) map[pcTrim(r[di])] = pcTrim(r[ci]); }
              parsed.rows.forEach(r => { if (!r.acctName) r.acctName = map[r.docNo] || ''; });
            }
          }
          setItems(prev => prev.map(it => it.key === key ? Object.assign({}, it, {
            status: parsed.error ? 'error' : 'ready', rows: parsed.rows, kind: parsed.kind, error: parsed.error, sheet,
          }) : it));
        }).catch(err => {
          const msg = String((err && err.message) || err).toLowerCase();
          setItems(prev => prev.map(it => it.key === key ? Object.assign({}, it, { status: 'error',
            error: /password|encrypt/.test(msg) ? 'ไฟล์มีรหัส — เปิดใน Excel แล้ว Save As ใหม่ก่อน' : 'อ่านไฟล์ไม่ได้ — รองรับ .xlsx / .xls / .csv' }) : it));
        });
      });
    };
    const ready = items.filter(it => it.status === 'ready');
    const details = U.unionRows ? U.unionRows(ready.filter(it => it.kind === 'detail').map(it => it.rows), U.mangoRowKey) : [];
    const headers = U.unionRows ? U.unionRows(ready.filter(it => it.kind === 'header').map(it => it.rows), r => r.docNo) : [];
    const detDocs = {};
    details.forEach(d => { detDocs[d.docNo] = 1; });
    const merged = U.mergeMango ? U.mergeMango(details, headers).concat(details.length ? U.mergeMango([], headers.filter(h => !detDocs[h.docNo])) : []) : [];
    // ผู้รับเงินของใบ OF — ใช้กรองเอาเฉพาะ OF เงินสดย่อย (ไฟล์ export อาจมี OF อื่นปนมา)
    const payees = {};
    merged.forEach(r => {
      const k = pcPayeeKey(r.acctName);
      const g = payees[k] || (payees[k] = { key: k, docs: {}, amt: 0 });
      g.docs[r.docNo] = 1; g.amt = pcR2(g.amt + pcNum(r.amount));
    });
    const payeeList = Object.values(payees).sort((a, c) => c.amt - a.amt);
    const remembered = String(pcOvGet(PC_PAYEE_KEY) || '').split('|').filter(Boolean);
    const defPick = (() => {
      const s = {};
      payeeList.forEach(p => { if (remembered.indexOf(p.key) >= 0) s[p.key] = true; });
      if (Object.keys(s).length) return s;
      payeeList.forEach(p => { if (PC_PAYEE_HINT.test(p.key)) s[p.key] = true; });
      if (Object.keys(s).length) return s;
      const nonCard = payeeList.filter(p => !PC_PAYEE_NOT.test(p.key));
      if (nonCard.length === 1) s[nonCard[0].key] = true;
      return s;
    })();
    const sel = pick || defPick;
    const rows = merged.filter(r => sel[pcPayeeKey(r.acctName)]);
    const docNos = [...new Set(rows.map(r => r.docNo))];
    const exDocs = {};
    (existing || []).forEach(l => { exDocs[l.docNo] = 1; });
    const nNew = docNos.filter(d => !exDocs[d]).length;
    const total = pcR2(rows.reduce((s, r) => s + pcNum(r.amount), 0));
    const paidDocs = docNos.filter(d => rows.some(r => r.docNo === d && pcOfPaid(r))).length;
    return (
      <Modal open maxWidth={860} onClose={onClose} title="📒 นำเข้า OF เงินสดย่อยจาก Mango"
        footer={<>
          <span style={{ marginRight: 'auto', fontSize: 12, color: rows.length ? 'var(--good)' : 'var(--ink-500)' }}>
            {rows.length ? docNos.length + ' ใบ OF · ' + rows.length + ' บรรทัด · ' + pcMoney(total) + ' บาท · ใหม่ ' + nNew + ' ใบ · จ่ายคืนแล้ว ' + paidDocs + ' ใบ' : (merged.length ? 'เลือกผู้รับเงินที่เป็นเงินสดย่อยก่อน' : 'ยังไม่ได้เลือกไฟล์')}
          </span>
          <button className="btn btn-ghost" onClick={onClose}>ยกเลิก</button>
          <button className="btn btn-primary" disabled={!rows.length}
            onClick={() => { pcOvSet(PC_PAYEE_KEY, Object.keys(sel).filter(k => sel[k]).join('|')); onCommit(rows); }}>
            นำเข้า {docNos.length} ใบ
          </button>
        </>}>
        <div onDragOver={e => { e.preventDefault(); setDrag(true); }} onDragLeave={() => setDrag(false)}
          onDrop={e => { e.preventDefault(); setDrag(false); if (e.dataTransfer) addFiles(e.dataTransfer.files); }}
          onClick={() => inputRef.current && inputRef.current.click()}
          style={{ border: '2px dashed ' + (drag ? 'var(--brand-500)' : 'var(--line)'), borderRadius: 12, padding: '18px 16px', textAlign: 'center',
            cursor: 'pointer', marginBottom: 12, background: drag ? 'color-mix(in oklch, var(--brand-500) 9%, #fff)' : 'var(--ink-50)' }}>
          <div style={{ fontSize: 26 }}>📒</div>
          <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--ink-700)' }}>ลากไฟล์มาวางที่นี่ หรือกดเพื่อเลือก</div>
          <div style={{ fontSize: 11.5, color: 'var(--ink-500)', marginTop: 3 }}>
            รายงานชุดเดียวกับ Fleet Card — OF 9.3 (v_exp_rpt344 รายบรรทัด + ฝ่าย) และ OF ยอดรวม (v_exp_rpt130 เลข PV / วันที่จ่ายคืน) · ลากพร้อมกันได้
          </div>
          <input ref={inputRef} type="file" multiple accept=".xlsx,.xls,.csv,.txt" style={{ display: 'none' }}
            onChange={e => { addFiles(e.target.files); e.target.value = ''; }} />
        </div>
        {items.length > 0 && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 12 }}>
            {items.map(it => (
              <div key={it.key} style={{ display: 'flex', gap: 8, alignItems: 'center', fontSize: 12 }}>
                <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={it.name}>{it.name}</span>
                {it.status === 'reading' && <span style={{ color: 'var(--ink-400)' }}>⏳ กำลังอ่าน…</span>}
                {it.status === 'ready' && <PcTag tone="ok">{it.kind === 'detail' ? 'OF 9.3 · ' + it.rows.length + ' บรรทัด' : 'OF ยอดรวม · ' + it.rows.length + ' ใบ'}</PcTag>}
                {it.status === 'error' && <span style={{ color: 'var(--bad)' }}>✕ {it.error}</span>}
                <button className="btn btn-ghost" style={{ padding: '1px 7px', fontSize: 11, color: 'var(--bad)' }}
                  onClick={() => setItems(prev => prev.filter(x => x.key !== it.key))}>✕</button>
              </div>
            ))}
          </div>
        )}
        {payeeList.length > 0 && (
          <div style={{ border: '1px solid var(--line)', borderRadius: 10, padding: '10px 12px' }}>
            <div style={{ fontSize: 12.5, fontWeight: 700, marginBottom: 6 }}>ผู้รับเงินในใบ OF — ติ๊กเฉพาะที่เป็นการเบิกคืนเงินสดย่อย</div>
            {payeeList.map(p => (
              <label key={p.key} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5, padding: '3px 0', cursor: 'pointer' }}>
                <input type="checkbox" checked={!!sel[p.key]} onChange={e => setPick(Object.assign({}, sel, { [p.key]: e.target.checked }))} />
                <span style={{ flex: 1 }}>{p.key}{PC_PAYEE_NOT.test(p.key) ? <span style={{ color: 'var(--ink-500)' }}> (บัตรน้ำมัน/บัตรผู้บริหาร — นำเข้าที่แท็บนั้น)</span> : null}</span>
                <span style={{ color: 'var(--ink-500)' }}>{Object.keys(p.docs).length} ใบ · {pcMoney(p.amt)}</span>
              </label>
            ))}
            <div style={{ fontSize: 11, color: 'var(--ink-500)', marginTop: 6 }}>ระบบจำที่เลือกไว้ใช้ครั้งหน้า</div>
          </div>
        )}
        <div style={{ marginTop: 10, fontSize: 11.5, color: 'var(--ink-500)', lineHeight: 1.7 }}>
          • นำเข้าไฟล์เดิมซ้ำได้ — ใบ OF ที่อยู่ในไฟล์จะถูกแทนทั้งใบด้วยบรรทัดล่าสุด (แก้ใน Mango แล้วนำเข้าใหม่ได้เลย)<br />
          • นำเข้าเฉพาะ OF ยอดรวม = อัปเดตเลข PV / วันที่จ่ายคืน ของใบที่มีอยู่แล้ว ไม่แตะบรรทัด
        </div>
      </Modal>
    );
  }

  /* ── หน้าต่างนำเข้าไฟล์ทะเบียนคุม (Excel ทั้งปี) ── */
  function PcRegImportModal({ txns, onClose, onCommit }) {
    const [file, setFile] = useState(null);       // { name, sheets, sheetNames, sheet, parsed }
    const [drag, setDrag] = useState(false);
    const [err, setErr] = useState('');
    const inputRef = useRef(null);
    const readFile = (f) => {
      if (!f) return;
      setErr('');
      f.arrayBuffer().then(buf => {
        const wb = XLSX.read(buf, { type: 'array' });          // ไม่ใช้ cellDates — วันที่เป็นเลข serial ตรงตัว
        const sheets = {};
        wb.SheetNames.forEach(n => { sheets[n] = XLSX.utils.sheet_to_json(wb.Sheets[n], { header: 1, blankrows: true, defval: '', raw: true }); });
        let best = null;
        wb.SheetNames.forEach(n => {
          const p = pcParseRegister(sheets[n]);
          if (p.error) return;
          const score = p.rows.length;
          if (!best || score > best.score) best = { n, score, p };
        });
        if (!best) { setErr('ไม่พบชีตทะเบียนคุม (ต้องมีหัวตาราง วันที่จ่ายเงิน / เลขที่เอกสาร / ผู้เบิก / รับ / จ่าย)'); return; }
        setFile({ name: f.name, sheets, sheetNames: wb.SheetNames, sheet: best.n, parsed: best.p });
      }).catch(e => setErr('อ่านไฟล์ไม่ได้: ' + ((e && e.message) || e)));
    };
    const changeSheet = (n) => setFile(prev => Object.assign({}, prev, { sheet: n, parsed: pcParseRegister(prev.sheets[n]) }));
    const P = file && file.parsed;
    const stat = useMemo(() => {
      if (!P || P.error) return null;
      const by = (k) => P.rows.filter(r => r.kind === k);
      const sum = (rs) => pcR2(rs.reduce((s, r) => s + pcNum(r.amount), 0));
      const pays = by('pay'), ins = by('in');
      const unround = pays.filter(p => !p._round);
      // ลองคำนวณผลหลังนำเข้า (เฉพาะปีนี้) ด้วยตัวคำนวณจริง
      const sim = P.rows.map((r, i) => Object.assign({}, r, { id: 'sim' + i }));
      const inId = {};
      sim.forEach(r => { if (r.kind === 'in' && !inId[r.payDate]) inId[r.payDate] = r.id; });
      sim.forEach(r => { if (r.kind === 'pay') r.roundId = r._round ? (inId[r._round] || '') : ''; });
      const bb = pcBuild(sim, [], pcToday());
      const eq = pcEquation(bb, P.year);
      const okRounds = bb.rounds.filter(r => Math.abs(r.diff) < 0.01).length;
      const exYear = (txns || []).filter(t => pcYear(t.payDate) === P.year);
      return { pays, ins, payAmt: sum(pays), inAmt: sum(ins), open: by('open'), other: by('other'), refund: by('refund'),
        unround, unroundAmt: sum(unround), eq, okRounds, nRounds: bb.rounds.length, exYear };
    }, [P, txns]);
    return (
      <Modal open maxWidth={860} onClose={onClose} title="📥 นำเข้าทะเบียนคุมเงินสดย่อย (Excel)"
        footer={<>
          <span style={{ marginRight: 'auto', fontSize: 12, color: 'var(--ink-600)' }}>
            {stat ? 'ปี ' + P.year + ' · ' + P.rows.length + ' แถว' + (stat.exYear.length ? ' · ในระบบมีปีนี้อยู่แล้ว ' + stat.exYear.length + ' แถว (ทับแถวเดิม ไม่เพิ่มซ้ำ)' : '') : 'ยังไม่ได้เลือกไฟล์'}
          </span>
          <button className="btn btn-ghost" onClick={onClose}>ยกเลิก</button>
          <button className="btn btn-primary" disabled={!stat} onClick={() => onCommit(P)}>นำเข้าปี {P ? P.year : ''}</button>
        </>}>
        <div onDragOver={e => { e.preventDefault(); setDrag(true); }} onDragLeave={() => setDrag(false)}
          onDrop={e => { e.preventDefault(); setDrag(false); if (e.dataTransfer && e.dataTransfer.files[0]) readFile(e.dataTransfer.files[0]); }}
          onClick={() => inputRef.current && inputRef.current.click()}
          style={{ border: '2px dashed ' + (drag ? 'var(--brand-500)' : 'var(--line)'), borderRadius: 12, padding: '18px 16px', textAlign: 'center',
            cursor: 'pointer', marginBottom: 12, background: drag ? 'color-mix(in oklch, var(--brand-500) 9%, #fff)' : 'var(--ink-50)' }}>
          <div style={{ fontSize: 26 }}>📒</div>
          <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--ink-700)' }}>{file ? file.name : 'ลากไฟล์ STM+ทะเบียนคุม ปี….xlsx มาวาง หรือกดเพื่อเลือก'}</div>
          <div style={{ fontSize: 11.5, color: 'var(--ink-500)', marginTop: 3 }}>อ่านจากชีตทะเบียนคุม: วันที่ตรวจเอกสาร · วันที่จ่ายเงิน · เลขที่ · ผู้เบิก · รับ · จ่าย · ฝ่าย · วันที่เงินเข้า (คอลัมน์ข้างหลัง) · เลข OF · ไฟล์สแกน</div>
          <input ref={inputRef} type="file" accept=".xlsx,.xls" style={{ display: 'none' }} onChange={e => { readFile(e.target.files[0]); e.target.value = ''; }} />
        </div>
        {err && <div style={{ color: 'var(--bad)', fontSize: 12.5, marginBottom: 10 }}>✕ {err}</div>}
        {file && file.sheetNames.length > 1 && (
          <div style={{ display: 'flex', gap: 8, alignItems: 'center', fontSize: 12, marginBottom: 10 }}>
            ชีต
            <select className="select input" value={file.sheet} onChange={e => changeSheet(e.target.value)} style={{ fontSize: 12, padding: '3px 6px' }}>
              {file.sheetNames.map(n => <option key={n} value={n}>{n}</option>)}
            </select>
            {P && P.error && <span style={{ color: 'var(--bad)' }}>{P.error}</span>}
          </div>
        )}
        {stat && (
          <>
            <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 10 }}>
              <PcTag tone="mute">จ่ายออก {stat.pays.length} ใบ · {pcMoney(stat.payAmt)}</PcTag>
              <PcTag tone="mute">เงินเข้า {stat.ins.length} รอบ · {pcMoney(stat.inAmt)}</PcTag>
              {stat.open.length > 0 && <PcTag tone="mute">ตั้งวงเงิน/ยอดยกมา {pcMoney(stat.open.reduce((s, r) => s + r.amount, 0))}</PcTag>}
              {stat.other.length > 0 && <PcTag tone="mute">ดอกเบี้ย/รับอื่น {pcMoney(stat.other.reduce((s, r) => s + r.amount, 0))}</PcTag>}
              {stat.refund.length > 0 && <PcTag tone="mute">รับคืนเงินทอน {stat.refund.length} รายการ</PcTag>}
              <PcTag tone={stat.okRounds === stat.nRounds ? 'ok' : 'warn'}>รอบเงินเข้าตรงกับใบ {stat.okRounds}/{stat.nRounds} รอบ</PcTag>
              <PcTag tone="warn">ยังไม่ได้เงินคืน {stat.unround.length} ใบ · {pcMoney(stat.unroundAmt)}</PcTag>
            </div>
            {stat.eq && (
              <div style={{ fontSize: 12.5, padding: '8px 12px', borderRadius: 8, background: Math.abs(stat.eq.gap) < 0.01 ? 'var(--good-bg)' : 'var(--warn-bg)', marginBottom: 10 }}>
                หลังนำเข้า: เงินคงเหลือ <b>{pcMoney(stat.eq.cash)}</b> + ค้างรับคืน <b>{pcMoney(stat.eq.out)}</b> = <b>{pcMoney(stat.eq.actual)}</b>
                {' '}· ยอดตั้งต้น + ดอกเบี้ย/รับอื่น <b>{pcMoney(stat.eq.expected)}</b>
                {Math.abs(stat.eq.gap) < 0.01 ? ' ✓ ลงตัว' : ' · ต่าง ' + pcMoney(stat.eq.gap)}
              </div>
            )}
            {P.fixes.length > 0 && (
              <div style={{ border: '1px solid var(--line)', borderRadius: 10, padding: '10px 12px', fontSize: 12, lineHeight: 1.6 }}>
                <div style={{ fontWeight: 700, fontSize: 12.5, marginBottom: 4 }}>🔧 จุดที่ระบบปรับให้ตอนอ่านไฟล์ ({P.fixes.length})</div>
                {P.fixes.map((f, i) => (
                  <div key={i} style={{ padding: '3px 0', borderTop: i ? '1px dashed var(--ink-100)' : 'none' }}>
                    {f.kind === 'date' ? <span>📅 {f.text}</span> : (
                      <span>
                        {f.unfixed ? '⚠️' : '✓'} รอบ <b>{pcD(f.date)}</b> เงินเข้า {pcMoney(f.amount)} — {f.why}
                        {f.moved && f.moved.length > 0 && <span style={{ color: 'var(--ink-500)' }}> · ย้ายมา {f.moved.length} ใบ ({f.moved.slice(0, 12).map(m => m.docNo).join(', ')}{f.moved.length > 12 ? ' …' : ''})</span>}
                        {f.dropped && f.dropped.length > 0 && <span style={{ color: 'var(--ink-500)' }}> · เอาออก {f.dropped.length} ใบ ({f.dropped.map(m => m.docNo).join(', ')})</span>}
                      </span>
                    )}
                  </div>
                ))}
              </div>
            )}
            <div style={{ marginTop: 10, fontSize: 11.5, color: 'var(--ink-500)', lineHeight: 1.7 }}>
              • นำเข้าซ้ำได้ — ทับแถวเดิมตามเลขที่เอกสาร/วันที่ ไม่เพิ่มซ้ำ · ใบที่ผูก OF เองไว้และหมายเหตุในระบบยังอยู่<br />
              • ใบที่คีย์ในระบบแล้ว (วันที่ + เลขที่ + ยอดเดียวกัน) จะรวมเป็นแถวเดียวกัน
            </div>
          </>
        )}
      </Modal>
    );
  }

  /* ── หน้าหลักของแท็บเงินสดย่อย ─────────────────────────────────── */
  function PettyCashPanel({ toast, programBar }) {
    const ui0 = useMemo(pcUiLoad, []);
    const [txns, setTxns] = useState([]);
    const [ofs, setOfs] = useState([]);
    const [loading, setLoading] = useState(true);
    const [saving, setSaving] = useState(false);
    const [year, setYear] = useState(ui0.year || '');
    const [tab, setTab] = useState(ui0.tab || 'reg');           // reg | rounds | of | dept
    const [filter, setFilter] = useState(ui0.filter || 'open'); // open | none | wait | in | all
    const [ofFilter, setOfFilter] = useState('orphan');
    const [q, setQ] = useState('');
    const [month, setMonth] = useState('');
    const [sel, setSel] = useState({});
    const [payModal, setPayModal] = useState(null);            // { edit? }
    const [inModal, setInModal] = useState(null);              // { edit?, preset? }
    const [linkPay, setLinkPay] = useState(null);
    const [importKind, setImportKind] = useState(null);        // 'of' | 'reg'
    const [openRound, setOpenRound] = useState({});
    const [openDoc, setOpenDoc] = useState({});
    const canEdit = pcCan('canEdit'), canDelete = pcCan('canDelete');
    const say = (m) => { if (toast) toast(m); };
    const today = pcToday();

    const reload = () => {
      if (!window.WTPData || !WTPData.fetchSheetRows) { setLoading(false); return Promise.resolve(); }
      setLoading(true);
      return Promise.all([WTPData.fetchSheetRows(PC_TXN), WTPData.fetchSheetRows(PC_OF)]).then(([a, c]) => {
        setTxns(a || []); setOfs(c || []); setLoading(false);
      }).catch(err => { console.warn('[PettyCash] โหลดข้อมูลไม่สำเร็จ:', err && err.message); setLoading(false); });
    };
    useEffect(() => { reload(); }, []);
    useEffect(() => { pcUiSave({ year, tab, filter }); }, [year, tab, filter]);

    const b = useMemo(() => pcBuild(txns, ofs, today), [txns, ofs]);
    const years = useMemo(() => [...new Set(b.rows.map(t => pcYear(t.payDate)).filter(Boolean))].sort().reverse(), [b]);
    const Y = (year && years.indexOf(year) >= 0) ? year : (years.indexOf(today.slice(0, 4)) >= 0 ? today.slice(0, 4) : (years[0] || today.slice(0, 4)));
    const eq = useMemo(() => pcEquation(b, Y), [b, Y]);
    const inY = (t) => pcYear(t.payDate) === Y;

    const openPays = b.pays.filter(p => pcIsOpen(p) && pcYear(p.payDate) <= Y);
    const sumNet = (ps) => pcR2(ps.reduce((s, p) => s + p.net, 0));
    const kNone = openPays.filter(p => p.state === 'none');
    const kWait = openPays.filter(p => PC_WAIT[p.state]);
    const kPaid = openPays.filter(p => p.state === 'ofpaid');
    const kIn = b.pays.filter(p => p.state === 'in' && inY(p));
    const roundsY = b.rounds.filter(inY);
    const badRounds = roundsY.filter(r => Math.abs(r.diff) >= 0.01);
    const orphanDocs = b.ofDocs.filter(d => d.state === 'orphan' || d.state === 'partial');
    const oldestNone = kNone.reduce((m, p) => Math.max(m, p.age || 0), 0);

    // ตัวเลือกในฟอร์ม
    const whoDept = useMemo(() => {
      const m = {};
      b.pays.slice().sort(pcByDate).forEach(p => { if (pcTrim(p.requester) && pcTrim(p.deptCode)) m[pcTrim(p.requester)] = pcDept(p.deptCode); });
      return m;
    }, [b]);
    const whoOptions = useMemo(() => Object.keys(whoDept).sort((a, c) => a.localeCompare(c, 'th')), [whoDept]);
    const deptOptions = useMemo(() => {
      const s = {};
      b.pays.forEach(p => { if (p.deptCode) s[pcDept(p.deptCode)] = 1; });
      b.ofs.forEach(l => { if (l.deptCode) s[pcDept(l.deptCode)] = 1; });
      ['FIN', 'ACC', 'HRD', 'GSS', 'MNG', 'ITD', 'SAL', 'PMD', 'SOD', 'QCD'].forEach(d => { s[d] = 1; });
      return Object.keys(s).sort();
    }, [b]);

    /* ── บันทึก ── upsert ทั้งแถว (server เติมคอลัมน์ที่ไม่ได้ส่งเป็น null → ต้องส่งครบทุกคอลัมน์) */
    const persist = (saveRows, delIds, okMsg) => {
      if (!window.WTPData || !WTPData.upsertSheetRows) return Promise.resolve();
      const clean = (saveRows || []).map(r => pcClean(r, PC_TXN_COLS));
      setSaving(true);
      return WTPData.upsertSheetRows(PC_TXN, clean, r => r.id).then(() => {
        if (delIds && delIds.length) return WTPData.forceDeleteRows(PC_TXN, delIds).then(res => {
          if (res && res.ok === false) throw new Error(/403|สิทธิ/.test(res.error || '') ? 'ลบไม่ได้ — ต้องใช้สิทธิ์หัวหน้า' : res.error);
        });
      }).then(() => {
        setTxns(prev => {
          const map = {};
          prev.forEach(r => { map[r.id] = r; });
          clean.forEach(r => { map[r.id] = Object.assign({}, map[r.id] || {}, r); });
          (delIds || []).forEach(id => { delete map[id]; });
          return Object.values(map);
        });
        setSaving(false);
        if (okMsg) say(okMsg);
      }).catch(err => {
        setSaving(false);
        say('บันทึกไม่สำเร็จ: ' + ((err && err.message) || err));
        reload();
      });
    };
    const rawById = useMemo(() => { const m = {}; txns.forEach(t => { m[t.id] = t; }); return m; }, [txns]);
    const maxSeq = (y) => txns.filter(t => pcYear(t.payDate) === y).reduce((m, t) => Math.max(m, Number(t.seq) || 0), 0);

    const savePays = (rows, extra) => {
      if (payModal && payModal.edit) {
        const r = rows[0], prev = rawById[payModal.edit.id] || payModal.edit;
        const o = Object.assign({}, prev, {
          payDate: r.payDate, checkDate: r.checkDate || '', docNo: pcTrim(r.docNo), requester: pcTrim(r.requester), deptCode: pcDept(r.deptCode),
          amount: pcR2(r.amount), description: pcTrim(r.description), ofNo: pcTrim(r.ofNo), scanNote: pcTrim(r.scanNote), note: pcTrim(r.note),
          roundId: extra.roundId || '', status: extra.ignored ? 'ignored' : '', ym: String(r.payDate || '').slice(0, 7),
        });
        if (pcYear(prev.payDate) !== pcYear(r.payDate)) o.seq = maxSeq(pcYear(r.payDate)) + 1;
        return persist([o], null, 'บันทึกใบ ' + o.docNo + ' แล้ว').then(() => setPayModal(null));
      }
      const seqBase = {};
      const out = rows.map(r => {
        const y = pcYear(r.payDate);
        seqBase[y] = (seqBase[y] != null ? seqBase[y] : maxSeq(y)) + 1;
        return { id: pcNewId('p'), fund: PC_FUND, kind: 'pay', seq: seqBase[y], payDate: r.payDate, checkDate: r.checkDate || r.payDate,
          docNo: pcTrim(r.docNo), requester: pcTrim(r.requester), description: pcTrim(r.description), amount: pcR2(r.amount),
          deptCode: pcDept(r.deptCode), roundId: '', refDoc: '', ofNo: pcTrim(r.ofNo), ofIds: '', status: '', scanNote: '', note: '',
          source: 'manual', ym: String(r.payDate || '').slice(0, 7) };
      });
      return persist(out, null, 'บันทึกจ่ายออก ' + out.length + ' ใบ · ' + pcMoney(out.reduce((s, r) => s + r.amount, 0)) + ' บาท')
        .then(() => setPayModal(null));
    };
    const deletePay = (p) => persist([], [p.id], 'ลบใบ ' + (p.docNo || '') + ' แล้ว').then(() => setPayModal(null));

    const saveIn = (row, itemIds) => {
      const ed = inModal && inModal.edit;
      const prev = ed ? (rawById[ed.id] || ed) : null;
      const y = pcYear(row.payDate);
      const o = Object.assign({}, prev || { id: pcNewId(row.kind === 'in' ? 'i' : 'o'), fund: PC_FUND, source: 'manual', seq: maxSeq(y) + 1,
        docNo: '', requester: '', deptCode: '', roundId: '', ofNo: '', ofIds: '', status: '', scanNote: '' }, {
        kind: row.kind, payDate: row.payDate, amount: row.amount,
        description: pcTrim(row.description) || (row.kind === 'in' ? 'รับเงินสดย่อย' : PC_KINDS[row.kind].label),
        refDoc: row.kind === 'refund' ? pcTrim(row.refDoc) : '', note: pcTrim(row.note), ym: String(row.payDate || '').slice(0, 7),
      });
      if (prev && pcYear(prev.payDate) !== y) o.seq = maxSeq(y) + 1;
      const changes = [o];
      if (itemIds) {
        const want = {};
        itemIds.forEach(id => { want[id] = true; });
        txns.forEach(t => {
          if (t.kind !== 'pay') return;
          if (want[t.id] && t.roundId !== o.id) changes.push(Object.assign({}, t, { roundId: o.id }));
          else if (!want[t.id] && t.roundId === o.id) changes.push(Object.assign({}, t, { roundId: '' }));
        });
      }
      return persist(changes, null, row.kind === 'in'
        ? 'บันทึกเงินเข้า ' + pcD(o.payDate) + ' ' + pcMoney(o.amount) + ' · ' + (itemIds || []).length + ' ใบ'
        : 'บันทึก' + PC_KINDS[o.kind].label + ' แล้ว').then(() => { setInModal(null); setSel({}); });
    };
    const deleteIn = (row) => {
      const clear = txns.filter(t => t.kind === 'pay' && t.roundId === row.id).map(t => Object.assign({}, t, { roundId: '' }));
      return persist(clear, [row.id], 'ลบแถวเงินเข้าแล้ว').then(() => setInModal(null));
    };
    const saveLink = (pay, res) => {
      const prev = rawById[pay.id] || pay;
      const o = Object.assign({}, prev, { ofIds: (res.ofIds || []).join(','), ofNo: pcTrim(res.ofNo), status: res.ignored ? 'ignored' : '' });
      const changes = [o];
      // บรรทัดที่ย้ายมาจากใบอื่นที่ผูกเองไว้ → เอาออกจากใบนั้น
      const mine = {};
      (res.ofIds || []).forEach(id => { mine[id] = true; });
      txns.forEach(t => {
        if (t.id === pay.id || t.kind !== 'pay') return;
        const ids = pcIds(t.ofIds);
        if (ids.some(id => mine[id])) changes.push(Object.assign({}, t, { ofIds: ids.filter(id => !mine[id]).join(',') }));
      });
      if (!res.clear && res.ofIds && res.ofIds.length) {
        pcAliasLearn(pay.requester, b.ofs.filter(l => mine[l.id]).map(pcEmpKey));
      }
      return persist(changes, null, res.clear ? 'ให้ระบบจับคู่ใบ ' + (pay.docNo || '') + ' เอง' : 'ผูก OF ของใบ ' + (pay.docNo || '') + ' แล้ว').then(() => setLinkPay(null));
    };
    const bulkIgnore = (ids, on) => {
      const changes = ids.map(id => rawById[id]).filter(Boolean).map(t => Object.assign({}, t, { status: on ? 'ignored' : '' }));
      return persist(changes, null, (on ? 'ตั้ง "ไม่ต้องตั้ง OF" ' : 'คืนสถานะ ') + changes.length + ' ใบ').then(() => setSel({}));
    };

    /* นำเข้าไฟล์ทะเบียนคุม */
    const commitRegister = (P) => {
      const year0 = P.year;
      const exById = {};
      txns.forEach(t => { exById[t.id] = t; });
      const manualPool = txns.filter(t => pcYear(t.payDate) === year0 && t.source !== 'import');
      const used = {}, occ = {};
      const keyOf = (r) => {
        const k = r.kind === 'pay' ? 'p|' + (pcTrim(r.docNo) || '-') : r.kind + '|' + (r.payDate || '-') + '|' + pcC(r.amount);
        const n = (occ[k] = (occ[k] || 0) + 1);
        return 'pc|x|' + year0 + '|' + k + (n > 1 ? '#' + n : '');
      };
      const rows = P.rows.map(r => {
        let id = keyOf(r);
        if (!exById[id]) {
          const twin = manualPool.find(t => !used[t.id] && t.kind === r.kind && t.payDate === r.payDate && pcC(t.amount) === pcC(r.amount)
            && (r.kind !== 'pay' || pcTrim(t.docNo) === pcTrim(r.docNo)));
          if (twin) { id = twin.id; used[id] = 1; }
        }
        return Object.assign({}, r, { id });
      });
      const inIdByDate = {};
      rows.forEach(r => { if (r.kind === 'in' && !inIdByDate[r.payDate]) inIdByDate[r.payDate] = r.id; });
      const save = rows.map(r => {
        const prev = exById[r.id] || {};
        const o = Object.assign({}, prev, {
          id: r.id, fund: PC_FUND, kind: r.kind, seq: r.seq, payDate: r.payDate, checkDate: r.checkDate || prev.checkDate || '',
          docNo: r.docNo || '', requester: r.requester || '', description: r.description || prev.description || '',
          amount: r.amount, deptCode: r.deptCode || prev.deptCode || '', refDoc: r.refDoc || '',
          ofNo: r.ofNo || prev.ofNo || '', ofIds: prev.ofIds || '', status: prev.status || '',
          scanNote: r.scanNote || prev.scanNote || '', note: prev.note || r.note || '',
          source: prev.source === 'manual' ? 'manual' : 'import', ym: String(r.payDate || '').slice(0, 7),
        });
        if (r.kind === 'pay') o.roundId = (r._round && inIdByDate[r._round]) || prev.roundId || '';
        else o.roundId = '';
        return o;
      });
      return persist(save, null, 'นำเข้าทะเบียนคุมปี ' + year0 + ' · ' + save.length + ' แถว').then(() => { setImportKind(null); setYear(year0); });
    };
    /* นำเข้า OF — แทนบรรทัดของใบ OF ที่อยู่ในไฟล์ทั้งใบ · ไฟล์มีแต่หัวใบ = อัปเดตข้อมูลจ่ายคืนของบรรทัดเดิม */
    const commitOf = (lines) => {
      const U = FCU();
      const docs = {}, ex = {};
      lines.forEach(l => { (docs[l.docNo] = docs[l.docNo] || []).push(l); });
      ofs.forEach(l => { (ex[l.docNo] = ex[l.docNo] || []).push(l); });
      const save = [], del = [];
      Object.keys(docs).forEach(dn => {
        const ls = docs[dn], old = ex[dn] || [];
        if (ls.every(l => l.headerOnly) && old.some(o => Number(o.lineNo) > 0)) {
          const h = ls[0];
          old.forEach(o => { const n = Object.assign({}, o); PC_OF_HEAD.forEach(k => { if (h[k] != null && h[k] !== '') n[k] = h[k]; }); save.push(n); });
          return;
        }
        const occ = {}, keep = {};
        ls.forEach((l, i) => {
          const k = (U.hash || String)([l.expenseCode, pcC(l.amount), l.itemName, l.vendor, l.deptCode, l.empCode].join('~'));
          const n = (occ[k] = (occ[k] || 0) + 1);
          const id = 'pco|' + dn + '|' + k + (n > 1 ? '#' + n : '');
          const o = old.find(x => x.id === id) || {};
          keep[id] = true;
          save.push(Object.assign({}, l, { id, ym: l.ym || (U.ymFromDocNo ? U.ymFromDocNo(dn) : ''), lineNo: l.headerOnly ? 0 : i + 1,
            status: o.status || '', note: o.note || '' }));
        });
        old.forEach(o => { if (!keep[o.id]) del.push(o.id); });
      });
      const clean = save.map(r => pcClean(r, PC_OF_COLS));
      setSaving(true);
      return WTPData.upsertSheetRows(PC_OF, clean, r => r.id)
        .then(() => (del.length ? WTPData.forceDeleteRows(PC_OF, del) : null))
        .then(() => {
          setOfs(prev => {
            const map = {};
            prev.forEach(r => { map[r.id] = r; });
            del.forEach(id => { delete map[id]; });
            clean.forEach(r => { map[r.id] = r; });
            return Object.values(map);
          });
          setSaving(false); setImportKind(null);
          say('นำเข้า OF ' + Object.keys(docs).length + ' ใบ (' + clean.length + ' บรรทัด)' + (del.length ? ' · แทนบรรทัดเก่า ' + del.length : ''));
        }).catch(err => { setSaving(false); say('นำเข้า OF ไม่สำเร็จ: ' + ((err && err.message) || err)); reload(); });
    };
    const setOfStatus = (doc, status) => {
      const clean = doc.lines.map(l => pcClean(Object.assign({}, l, { status }), PC_OF_COLS));
      setSaving(true);
      WTPData.upsertSheetRows(PC_OF, clean, r => r.id).then(() => {
        setOfs(prev => prev.map(l => (l.docNo === doc.docNo ? Object.assign({}, l, { status }) : l)));
        setSaving(false); say(status === 'ignored' ? 'ซ่อน ' + doc.docNo + ' (ไม่เกี่ยวกับเงินสดย่อย)' : 'คืน ' + doc.docNo + ' กลับมาติดตาม');
      }).catch(err => { setSaving(false); say('บันทึกไม่สำเร็จ: ' + ((err && err.message) || err)); });
    };

    /* ── ทะเบียน: แถวที่แสดงตามตัวกรอง ── */
    const qq = pcTrim(q).toLowerCase();
    const matchQ = (t) => !qq || [t.docNo, t.requester, t.description, t.deptCode, t.ofNo, t.note, pcR2(t.amount)]
      .concat(t.ofDocs || []).join(' ').toLowerCase().indexOf(qq) >= 0;
    const monthsY = useMemo(() => [...new Set(b.rows.filter(inY).map(t => String(t.payDate || '').slice(0, 7)).filter(Boolean))].sort(), [b, Y]);
    const regRows = useMemo(() => {
      let list;
      if (filter === 'all') list = b.rows.filter(inY).map(t => (t.kind === 'pay' ? b.payById[t.id] : t));
      else {
        const st = { open: (p) => pcIsOpen(p), none: (p) => p.state === 'none', wait: (p) => !!PC_WAIT[p.state], in: (p) => p.state === 'in', ignored: (p) => p.state === 'ignored' }[filter] || (() => true);
        list = b.pays.filter(p => st(p) && (filter === 'in' || filter === 'ignored' ? inY(p) : pcYear(p.payDate) <= Y));
      }
      return list.filter(t => (!month || String(t.payDate || '').slice(0, 7) === month) && matchQ(t));
    }, [b, filter, Y, month, qq]);
    const selIds = Object.keys(sel).filter(id => sel[id] && b.payById[id]);
    const selPays = selIds.map(id => b.payById[id]);
    const selOpen = selPays.filter(pcIsOpen);

    if (loading) {
      return <div className="page">{programBar}<div className="card" style={{ padding: 24, color: 'var(--ink-500)', fontSize: 13 }}>⏳ กำลังโหลดทะเบียนเงินสดย่อย…</div></div>;
    }
    const empty = b.rows.length === 0;
    const filters = [
      { k: 'open', label: 'ค้างรับคืน', n: openPays.length },
      { k: 'none', label: 'ยังไม่ตั้ง OF', n: kNone.length },
      { k: 'wait', label: 'ตั้ง OF แล้ว รอเงินเข้า', n: kWait.length },
      { k: 'in', label: 'เงินเข้าแล้ว', n: kIn.length },
      { k: 'all', label: 'ทะเบียนเต็ม (รับ/จ่าย/คงเหลือ)', n: b.rows.filter(inY).length },
    ];
    if (b.pays.some(p => p.state === 'ignored')) filters.push({ k: 'ignored', label: 'ไม่ต้องตั้ง OF', n: b.pays.filter(p => p.state === 'ignored' && inY(p)).length });
    const goReg = (f) => { setTab('reg'); setFilter(f); };
    const ofDocsY = b.ofDocs.filter(d => pcYear(d.ym || d.date) === Y || d.state === 'orphan' || d.state === 'partial');
    const ofShown = ofDocsY.filter(d => ofFilter === 'all' ? true : ofFilter === 'orphan' ? (d.state === 'orphan' || d.state === 'partial')
      : ofFilter === 'unpaid' ? (!d.paid && d.state !== 'ignored') : ofFilter === 'ignored' ? d.state === 'ignored' : true);

    return (
      <div className="page">
        <div className="page-head">
          <div>
            <div className="page-title">💵 เงินสดย่อย</div>
            <div className="page-sub">
              ทะเบียนคุมวงเงินสดย่อย — จ่ายออกรายใบ → ผู้เบิกตั้ง OF ใน Mango → บัญชีจ่ายคืนเข้าวงเงินเป็นรอบ
              {saving ? ' • กำลังบันทึก…' : ''}
            </div>
          </div>
          <div className="page-head-r" style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
            {canEdit && (
              <>
                <button className="btn btn-primary" onClick={() => setPayModal({})}><Icon name="plus" size={14} /> บันทึกจ่ายออก</button>
                <button className="btn" onClick={() => setInModal({})}><Icon name="plus" size={14} /> บันทึกเงินเข้า</button>
                <button className="btn" onClick={() => setImportKind('of')}><Icon name="download" size={14} /> นำเข้า OF จาก Mango</button>
                <button className="btn btn-ghost" onClick={() => setImportKind('reg')} title="นำเข้าไฟล์ STM+ทะเบียนคุม ปี….xlsx ทั้งปี">
                  <Icon name="upload" size={14} /> นำเข้าทะเบียนคุม (Excel)
                </button>
              </>
            )}
            {!empty && eq && (
              <button className="btn btn-ghost" onClick={() => pcExportExcel(b, Y, eq)} title="ส่งออกทะเบียนคุม + รอบเงินเข้า + ค้างรับคืน + สรุปรายฝ่าย">
                <Icon name="download" size={14} /> Excel
              </button>
            )}
            <button className="btn btn-ghost" onClick={reload} title="โหลดข้อมูลใหม่"><Icon name="refresh" size={14} /> รีเฟรช</button>
          </div>
        </div>

        {programBar}

        {empty ? (
          <div className="card" style={{ padding: '28px 24px', textAlign: 'center' }}>
            <div style={{ fontSize: 34 }}>💵</div>
            <div style={{ fontSize: 15, fontWeight: 800, marginTop: 4 }}>ยังไม่มีข้อมูลเงินสดย่อย</div>
            <div style={{ fontSize: 12.5, color: 'var(--ink-500)', margin: '6px auto 14px', maxWidth: 560, lineHeight: 1.7 }}>
              เริ่มจากนำเข้าไฟล์ทะเบียนคุมทั้งปี (STM+ทะเบียนคุม ปี….xlsx) — ระบบอ่านใบจ่าย รอบเงินเข้า และวันที่แต่ละใบเข้ารอบไหนให้
              แล้วต่อจากนั้นบันทึกจ่ายออก/เงินเข้าในหน้านี้ได้เลย
            </div>
            {canEdit && (
              <div style={{ display: 'flex', gap: 8, justifyContent: 'center', flexWrap: 'wrap' }}>
                <button className="btn btn-primary" onClick={() => setImportKind('reg')}><Icon name="upload" size={14} /> นำเข้าทะเบียนคุม (Excel)</button>
                <button className="btn" onClick={() => setPayModal({})}><Icon name="plus" size={14} /> บันทึกจ่ายออก</button>
              </div>
            )}
          </div>
        ) : (
          <>
            {/* ปี + สมการวงเงิน */}
            <div className="card" style={{ padding: '10px 14px', marginBottom: 12, display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
              <span style={{ fontSize: 12.5, fontWeight: 700, color: 'var(--ink-700)' }}>ปี</span>
              {years.map(y => <button key={y} style={pcChip(y === Y)} onClick={() => { setYear(y); setMonth(''); }}>{y}</button>)}
              {eq && (
                <span style={{ marginLeft: 'auto', fontSize: 12.5, color: 'var(--ink-700)' }}
                  title={'ยอดตั้งต้นปี ' + pcMoney(eq.startCash + eq.startOut) + ' (เงินสด ' + pcMoney(eq.startCash) + (eq.startOut ? ' + ค้างรับคืนยกมา ' + pcMoney(eq.startOut) : '') + ') + ดอกเบี้ย/รับอื่น ' + pcMoney(eq.other + eq.refundUn)}>
                  เงินคงเหลือ <b>{pcMoney(eq.cash)}</b> + ค้างรับคืน <b>{pcMoney(eq.out)}</b>{eq.ignored ? <> + ไม่ต้องตั้ง OF <b>{pcMoney(eq.ignored)}</b></> : null}
                  {' '}= <b>{pcMoney(eq.actual)}</b>
                  {Math.abs(eq.gap) < 0.01
                    ? <span style={{ color: 'var(--good)', fontWeight: 700 }}> ✓ ลงตัวกับวงเงิน {pcMoney(eq.fundLimit)} + ดอกเบี้ย/รับอื่น {pcMoney(eq.expected - eq.fundLimit)}</span>
                    : <span style={{ color: 'var(--bad)', fontWeight: 700, cursor: 'pointer' }} onClick={() => setTab('rounds')}> · ไม่ลงตัว ต่าง {pcMoney(eq.gap)} (ควรเป็น {pcMoney(eq.expected)}) — ดูรอบที่ยอดไม่ตรง</span>}
                </span>
              )}
            </div>

            <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', marginBottom: 12 }}>
              <PcTile label="เงินคงเหลือในวงเงิน" value={eq ? pcMoney(eq.cash) : '—'} sub={eq ? 'วงเงิน ' + pcMoney(eq.fundLimit) + ' · ใช้ไปรอรับคืน ' + pcMoney(eq.out) : ''} active={false} />
              <PcTile label="🔴 โอนแล้ว ยังไม่ตั้ง OF" tone="bad" value={pcMoney(sumNet(kNone))}
                sub={kNone.length + ' ใบ' + (kNone.length ? ' · เก่าสุด ' + oldestNone + ' วัน' : '')}
                active={tab === 'reg' && filter === 'none'} onClick={() => goReg('none')} />
              <PcTile label="🟠 ตั้ง OF แล้ว รอเงินเข้า" tone="warn" value={pcMoney(sumNet(kWait))}
                sub={kWait.length + ' ใบ' + (kPaid.length ? ' · บัญชีจ่ายแล้ว ' + kPaid.length + ' ใบ (ยังไม่บันทึกเงินเข้า)' : '')}
                active={tab === 'reg' && filter === 'wait'} onClick={() => goReg('wait')} />
              <PcTile label={'🟢 เงินเข้าแล้ว ปี ' + Y} tone="ok" value={pcMoney(sumNet(kIn))}
                sub={kIn.length + ' ใบ · ' + roundsY.length + ' รอบ' + (badRounds.length ? ' · ยอดไม่ตรง ' + badRounds.length + ' รอบ' : '')}
                active={tab === 'rounds'} onClick={() => setTab('rounds')} />
            </div>

            {(badRounds.length > 0 || orphanDocs.length > 0 || kPaid.length > 0) && (
              <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 12 }}>
                {badRounds.length > 0 && <PcTag tone="bad" onClick={() => setTab('rounds')}>⚠ เงินเข้า {badRounds.length} รอบ ยอดไม่ตรงกับใบที่ผูก / ยังไม่ระบุใบ</PcTag>}
                {kPaid.length > 0 && <PcTag tone="info" onClick={() => goReg('wait')}>🔵 บัญชีจ่าย OF แล้ว {kPaid.length} ใบ ({pcMoney(sumNet(kPaid))}) — ยังไม่บันทึกเงินเข้า</PcTag>}
                {orphanDocs.length > 0 && <PcTag tone="warn" onClick={() => { setTab('of'); setOfFilter('orphan'); }}>📑 OF ที่หาใบจ่ายไม่เจอ {orphanDocs.length} ใบ ({pcMoney(orphanDocs.reduce((s, d) => s + d.freeAmt, 0))})</PcTag>}
              </div>
            )}

            <div style={{ display: 'flex', gap: 4, marginBottom: 10, borderBottom: '1px solid var(--line)' }}>
              {[
                { k: 'reg', label: '📒 ทะเบียนคุม' },
                { k: 'rounds', label: '💰 รอบเงินเข้า (' + roundsY.length + ')' },
                { k: 'of', label: '📑 OF จาก Mango (' + b.ofDocs.length + ')' },
                { k: 'dept', label: '📊 สรุปรายฝ่าย' },
              ].map(t => (
                <button key={t.k} onClick={() => setTab(t.k)}
                  style={{ border: 'none', background: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 13, fontWeight: 700, padding: '8px 12px',
                    color: tab === t.k ? 'var(--brand-700)' : 'var(--ink-500)', borderBottom: '2px solid ' + (tab === t.k ? 'var(--brand-500)' : 'transparent'), marginBottom: -1 }}>
                  {t.label}
                </button>
              ))}
            </div>

            {tab === 'reg' && (
              <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
                <div style={{ padding: '10px 12px', display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center', borderBottom: '1px solid var(--line)' }}>
                  {filters.map(f => <button key={f.k} style={pcChip(filter === f.k)} onClick={() => { setFilter(f.k); setSel({}); }}>{f.label} ({f.n})</button>)}
                  <span style={{ marginLeft: 'auto', display: 'flex', gap: 6 }}>
                    <select className="select input" value={month} onChange={e => setMonth(e.target.value)} style={{ fontSize: 12, padding: '4px 6px' }}>
                      <option value="">ทุกเดือน</option>
                      {monthsY.map(mm => <option key={mm} value={mm}>{PC_MONTHS[Number(mm.slice(5, 7)) - 1] || mm} {mm.slice(0, 4)}</option>)}
                    </select>
                    <input className="input" placeholder="ค้นหา เลขที่ / ผู้เบิก / ฝ่าย / OF / ยอด" value={q} onChange={e => setQ(e.target.value)} style={{ width: 230, fontSize: 12 }} />
                  </span>
                </div>
                {canEdit && selIds.length > 0 && (
                  <div style={{ padding: '8px 12px', display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', background: 'color-mix(in srgb, var(--brand-500) 7%, white)', borderBottom: '1px solid var(--line)', fontSize: 12.5 }}>
                    <b>เลือก {selIds.length} ใบ · {pcMoney(sumNet(selPays))}</b>
                    {selOpen.length > 0 && <button className="btn btn-primary" style={{ padding: '4px 10px', fontSize: 12 }} onClick={() => setInModal({ preset: selOpen.map(p => p.id) })}>💰 บันทึกเงินเข้าของใบที่เลือก</button>}
                    {selPays.some(p => p.state !== 'ignored' && p.state !== 'in') && <button className="btn" style={{ padding: '4px 10px', fontSize: 12 }} onClick={() => bulkIgnore(selPays.filter(p => p.state !== 'in').map(p => p.id), true)}>ไม่ต้องตั้ง OF</button>}
                    {selPays.some(p => p.state === 'ignored') && <button className="btn" style={{ padding: '4px 10px', fontSize: 12 }} onClick={() => bulkIgnore(selPays.filter(p => p.state === 'ignored').map(p => p.id), false)}>คืนเป็นต้องตั้ง OF</button>}
                    <button className="btn btn-ghost" style={{ padding: '4px 10px', fontSize: 12 }} onClick={() => setSel({})}>ล้างที่เลือก</button>
                  </div>
                )}
                <div style={{ maxHeight: 'calc(100vh - 330px)', minHeight: 240, overflow: 'auto' }}>
                  <table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 900 }}>
                    <thead><tr>
                      {canEdit && <th style={pcTh('center', 30)}>
                        <input type="checkbox" checked={regRows.some(t => t.kind === 'pay') && regRows.filter(t => t.kind === 'pay').every(t => sel[t.id])}
                          onChange={e => { const n = {}; if (e.target.checked) regRows.forEach(t => { if (t.kind === 'pay') n[t.id] = true; }); setSel(n); }} />
                      </th>}
                      <th style={pcTh('right', 44)}>ลำดับ</th><th style={pcTh('left', 86)}>วันที่จ่าย</th><th style={pcTh('left', 70)}>เลขที่</th>
                      <th style={pcTh('left')}>ผู้เบิก / รายการ</th><th style={pcTh('left', 50)}>ฝ่าย</th>
                      {filter === 'all' && <th style={pcTh('right', 92)}>รับ</th>}<th style={pcTh('right', 92)}>จ่าย</th>
                      {filter === 'all' && <th style={pcTh('right', 96)}>คงเหลือ</th>}
                      <th style={pcTh('left')}>OF</th><th style={pcTh('left')}>สถานะ</th>
                      {canEdit && <th style={pcTh('center', 36)}></th>}
                    </tr></thead>
                    <tbody>
                      {regRows.length === 0 && <tr><td colSpan={12} style={pcTd('center', { padding: 26, color: 'var(--ink-500)' })}>
                        {filter === 'open' ? '🎉 ไม่มีใบค้างรับคืน' : 'ไม่มีรายการตามตัวกรอง'}</td></tr>}
                      {regRows.map(t => {
                        if (t.kind !== 'pay') {
                          const r = t.kind === 'in' ? b.rounds.find(x => x.id === t.id) : null;
                          return (
                            <tr key={t.id} style={{ background: t.kind === 'in' ? 'color-mix(in srgb, var(--good) 7%, white)' : 'var(--ink-50)' }}>
                              {canEdit && <td style={pcTd()}></td>}
                              <td style={pcTd('right', { color: 'var(--ink-500)' })}>{t.seq || ''}</td>
                              <td style={pcTd()}>{pcD(t.payDate)}</td><td style={pcTd()}>{t.docNo || ''}</td>
                              <td style={pcTd('left', { fontWeight: 700 })}>{t.description || PC_KINDS[t.kind].label}{t.refDoc ? ' (ใบ ' + t.refDoc + ')' : ''}</td>
                              <td style={pcTd()}></td>
                              <td style={pcTd('right', { fontWeight: 700, color: 'var(--good)' })}>{pcMoney(t.amount)}</td><td style={pcTd('right')}></td>
                              {filter === 'all' && <td style={pcTd('right', { fontWeight: 600 })}>{pcMoney(b.balOf[t.id])}</td>}
                              <td style={pcTd()}></td>
                              <td style={pcTd()}>
                                {r ? (
                                  <PcTag tone={!r.items.length ? 'bad' : Math.abs(r.diff) < 0.01 ? 'ok' : 'warn'}
                                    onClick={() => { setTab('rounds'); setOpenRound({ [r.id]: true }); }}>
                                    {!r.items.length ? 'ยังไม่ระบุว่าเป็นของใบไหน' : r.items.length + ' ใบ' + (Math.abs(r.diff) < 0.01 ? ' ✓ ตรง' : ' · ต่าง ' + pcMoney(r.diff))}
                                  </PcTag>
                                ) : <PcTag tone="mute">{PC_KINDS[t.kind].label}</PcTag>}
                              </td>
                              {canEdit && <td style={pcTd('center')}><button className="btn btn-ghost" style={{ padding: '2px 6px' }} onClick={() => setInModal({ edit: t })} title="แก้ไข"><Icon name="edit" size={13} /></button></td>}
                            </tr>
                          );
                        }
                        const p = t;
                        return (
                          <tr key={p.id} style={{ background: sel[p.id] ? 'color-mix(in srgb, var(--brand-500) 7%, white)' : undefined }}>
                            {canEdit && <td style={pcTd('center')}><input type="checkbox" checked={!!sel[p.id]} onChange={() => setSel(prev => Object.assign({}, prev, { [p.id]: !prev[p.id] }))} /></td>}
                            <td style={pcTd('right', { color: 'var(--ink-500)' })}>{p.seq || ''}</td>
                            <td style={pcTd()}>{pcD(p.payDate)}</td>
                            <td style={pcTd('left', { fontWeight: 600 })}>{p.docNo}</td>
                            <td style={pcTd()}>{p.requester}{p.description ? <span style={{ color: 'var(--ink-500)' }}> · {p.description}</span> : null}
                              {p.note ? <div style={{ fontSize: 10.5, color: 'var(--ink-500)' }}>{p.note}</div> : null}</td>
                            <td style={pcTd()}>{p.deptCode}</td>
                            {filter === 'all' && <td style={pcTd('right')}></td>}
                            <td style={pcTd('right', { fontWeight: 700 })}>{pcMoney(p.amount)}{p.refund ? <div style={{ fontSize: 10.5, color: 'var(--ink-500)', fontWeight: 400 }}>เงินทอนคืน {pcMoney(p.refund)}</div> : null}</td>
                            {filter === 'all' && <td style={pcTd('right', { color: 'var(--ink-600)' })}>{pcMoney(b.balOf[p.id])}</td>}
                            <td style={pcTd('left', { fontSize: 11.5 })}><PcOfCell p={p} onClick={canEdit ? () => setLinkPay(p) : null} /></td>
                            <td style={pcTd()}><PcPayState p={p} onRound={(id) => { setTab('rounds'); setOpenRound({ [id]: true }); }} /></td>
                            {canEdit && <td style={pcTd('center')}><button className="btn btn-ghost" style={{ padding: '2px 6px' }} onClick={() => setPayModal({ edit: rawById[p.id] || p })} title="แก้ไข"><Icon name="edit" size={13} /></button></td>}
                          </tr>
                        );
                      })}
                    </tbody>
                    {filter !== 'all' && regRows.length > 0 && (
                      <tfoot><tr>
                        <td colSpan={canEdit ? 6 : 5} style={pcTd('right', { fontWeight: 700, background: 'var(--ink-50)' })}>รวม {regRows.length} ใบ</td>
                        <td style={pcTd('right', { fontWeight: 800, background: 'var(--ink-50)' })}>{pcMoney(regRows.reduce((s, p) => s + pcNum(p.amount), 0))}</td>
                        <td colSpan={canEdit ? 3 : 2} style={pcTd('left', { background: 'var(--ink-50)' })}></td>
                      </tr></tfoot>
                    )}
                  </table>
                </div>
              </div>
            )}

            {tab === 'rounds' && (
              <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
                <div style={{ padding: '10px 14px', fontSize: 12.5, color: 'var(--ink-600)', borderBottom: '1px solid var(--line)', display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
                  <span>เงินเข้าแต่ละรอบเป็นของใบไหน — กดที่แถวเพื่อดูรายการใบ</span>
                  {canEdit && <button className="btn btn-primary" style={{ marginLeft: 'auto', padding: '4px 11px', fontSize: 12 }} onClick={() => setInModal({})}><Icon name="plus" size={13} /> บันทึกเงินเข้า</button>}
                </div>
                <div style={{ overflowX: 'auto' }}>
                <table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 760 }}>
                  <thead><tr>
                    <th style={pcTh('center', 26)}></th><th style={pcTh('left', 100)}>วันที่เงินเข้า</th><th style={pcTh('right', 110)}>ยอดเงินเข้า</th>
                    <th style={pcTh('right', 60)}>ใบ</th><th style={pcTh('right', 110)}>ยอดใบที่ผูก</th><th style={pcTh('left', 160)}>ตรวจยอด</th>
                    <th style={pcTh('left')}>ฝ่าย</th>{canEdit && <th style={pcTh('center', 60)}></th>}
                  </tr></thead>
                  <tbody>
                    {roundsY.length === 0 && <tr><td colSpan={8} style={pcTd('center', { padding: 24, color: 'var(--ink-500)' })}>ยังไม่มีเงินเข้าในปี {Y}</td></tr>}
                    {roundsY.slice().sort((a, c) => (a.payDate < c.payDate ? 1 : -1)).map(r => {
                      const open = !!openRound[r.id];
                      const depts = {};
                      r.items.forEach(p => { const k = p.deptCode || '—'; depts[k] = pcR2((depts[k] || 0) + p.net); });
                      return (
                        <React.Fragment key={r.id}>
                          <tr style={{ cursor: 'pointer', background: open ? 'var(--ink-50)' : undefined }} onClick={() => setOpenRound(prev => Object.assign({}, prev, { [r.id]: !prev[r.id] }))}>
                            <td style={pcTd('center', { color: 'var(--ink-500)' })}>{open ? '▾' : '▸'}</td>
                            <td style={pcTd('left', { fontWeight: 700 })}>{pcD(r.payDate)}</td>
                            <td style={pcTd('right', { fontWeight: 700 })}>{pcMoney(r.amount)}</td>
                            <td style={pcTd('right')}>{r.items.length}</td>
                            <td style={pcTd('right')}>{pcMoney(r.itemsNet)}</td>
                            <td style={pcTd()}>
                              {!r.items.length ? <PcTag tone="bad">ยังไม่ระบุว่าเป็นของใบไหน</PcTag>
                                : Math.abs(r.diff) < 0.01 ? <PcTag tone="ok">✓ ตรง</PcTag> : <PcTag tone="warn">ต่าง {pcMoney(r.diff)}</PcTag>}
                            </td>
                            <td style={pcTd('left', { fontSize: 11.5, color: 'var(--ink-600)' })}>{Object.keys(depts).sort((a, c) => depts[c] - depts[a]).map(k => k + ' ' + pcMoney(depts[k])).join(' · ')}</td>
                            {canEdit && <td style={pcTd('center')} onClick={e => e.stopPropagation()}>
                              <button className="btn btn-ghost" style={{ padding: '2px 8px', fontSize: 11.5 }} onClick={() => setInModal({ edit: rawById[r.id] || r })}>จัดใบ</button>
                            </td>}
                          </tr>
                          {open && (
                            <tr><td colSpan={8} style={{ padding: '0 0 10px 34px', background: 'var(--ink-50)' }}>
                              {r.items.length === 0 ? <div style={{ fontSize: 12, color: 'var(--ink-500)', padding: 8 }}>ยังไม่ได้ผูกใบ — กด "จัดใบ" แล้วให้ระบบเลือกให้</div> : (
                                <table style={{ width: '100%', borderCollapse: 'collapse', background: '#fff', borderRadius: 8 }}>
                                  <thead><tr>
                                    <th style={pcTh('left', 90)}>วันที่จ่าย</th><th style={pcTh('left', 72)}>เลขที่</th><th style={pcTh('left')}>ผู้เบิก</th>
                                    <th style={pcTh('left', 56)}>ฝ่าย</th><th style={pcTh('right', 100)}>ยอด</th><th style={pcTh('left')}>OF</th>
                                  </tr></thead>
                                  <tbody>
                                    {r.items.map(p => (
                                      <tr key={p.id}>
                                        <td style={pcTd()}>{pcD(p.payDate)}</td><td style={pcTd()}>{p.docNo}</td><td style={pcTd()}>{p.requester}</td>
                                        <td style={pcTd()}>{p.deptCode}</td>
                                        <td style={pcTd('right')}>{pcMoney(p.net)}{p.refund ? <span style={{ fontSize: 10.5, color: 'var(--ink-500)' }}> (หักเงินทอน {pcMoney(p.refund)})</span> : null}</td>
                                        <td style={pcTd('left', { fontSize: 11.5 })}><PcOfCell p={p} /></td>
                                      </tr>
                                    ))}
                                  </tbody>
                                </table>
                              )}
                            </td></tr>
                          )}
                        </React.Fragment>
                      );
                    })}
                  </tbody>
                </table>
                </div>
              </div>
            )}

            {tab === 'of' && (
              <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
                <div style={{ padding: '10px 12px', display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center', borderBottom: '1px solid var(--line)' }}>
                  {[
                    { k: 'orphan', label: 'หาใบจ่ายไม่เจอ', n: orphanDocs.length },
                    { k: 'unpaid', label: 'บัญชียังไม่จ่ายคืน', n: ofDocsY.filter(d => !d.paid && d.state !== 'ignored').length },
                    { k: 'all', label: 'ทั้งหมด', n: ofDocsY.length },
                    { k: 'ignored', label: 'ซ่อนไว้', n: ofDocsY.filter(d => d.state === 'ignored').length },
                  ].map(f => <button key={f.k} style={pcChip(ofFilter === f.k)} onClick={() => setOfFilter(f.k)}>{f.label} ({f.n})</button>)}
                  {canEdit && <button className="btn" style={{ marginLeft: 'auto', padding: '4px 11px', fontSize: 12 }} onClick={() => setImportKind('of')}><Icon name="download" size={13} /> นำเข้า OF จาก Mango</button>}
                </div>
                {b.ofDocs.length === 0 ? (
                  <div style={{ padding: 26, textAlign: 'center', color: 'var(--ink-500)', fontSize: 12.5, lineHeight: 1.7 }}>
                    ยังไม่ได้นำเข้า OF — export รายงาน OF 9.3 (v_exp_rpt344) + OF ยอดรวม (v_exp_rpt130) จาก Mango แล้วกด "นำเข้า OF จาก Mango"<br />
                    ระบบจะจับคู่บรรทัด OF กับใบจ่ายให้เอง (ยอด + ฝ่าย + วันที่) เพื่อบอกว่าใบไหนยังไม่มีคนตั้ง OF
                  </div>
                ) : (
                  <div style={{ maxHeight: 'calc(100vh - 330px)', minHeight: 240, overflow: 'auto' }}>
                    <table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 860 }}>
                      <thead><tr>
                        <th style={pcTh('center', 26)}></th><th style={pcTh('left', 116)}>เลข OF</th><th style={pcTh('left', 86)}>วันที่</th>
                        <th style={pcTh('left')}>ผู้ตั้ง</th><th style={pcTh('left', 70)}>ฝ่าย</th><th style={pcTh('right', 100)}>ยอด</th>
                        <th style={pcTh('left', 150)}>จ่ายคืน</th><th style={pcTh('left')}>ผูกกับใบจ่าย</th>{canEdit && <th style={pcTh('center', 70)}></th>}
                      </tr></thead>
                      <tbody>
                        {ofShown.length === 0 && <tr><td colSpan={9} style={pcTd('center', { padding: 24, color: 'var(--ink-500)' })}>{ofFilter === 'orphan' ? '✓ OF ทุกใบผูกกับใบจ่ายได้แล้ว' : 'ไม่มีรายการ'}</td></tr>}
                        {ofShown.map(d => {
                          const open = !!openDoc[d.docNo];
                          return (
                            <React.Fragment key={d.docNo}>
                              <tr style={{ cursor: 'pointer', opacity: d.state === 'ignored' ? 0.55 : 1 }} onClick={() => setOpenDoc(prev => Object.assign({}, prev, { [d.docNo]: !prev[d.docNo] }))}>
                                <td style={pcTd('center', { color: 'var(--ink-500)' })}>{open ? '▾' : '▸'}</td>
                                <td style={pcTd('left', { fontWeight: 700 })}>{d.docNo}</td>
                                <td style={pcTd()}>{pcD(d.date)}</td>
                                <td style={pcTd('left', { fontSize: 11.5 })}>{d.emp}</td>
                                <td style={pcTd()}>{d.depts.join(', ')}</td>
                                <td style={pcTd('right', { fontWeight: 700 })}>{pcMoney(d.amount)}</td>
                                <td style={pcTd('left', { fontSize: 11.5 })}>{d.paid ? <span style={{ color: 'var(--good)' }}>✓ {pcD(d.payDate)}{d.pvNo ? ' · ' + d.pvNo : ''}</span> : <span style={{ color: 'var(--ink-500)' }}>ยังไม่จ่าย</span>}</td>
                                <td style={pcTd('left', { fontSize: 11.5 })}>
                                  {d.owners.map(p => p.docNo + ' ' + (p.requester || '')).join(' · ')}
                                  {d.state === 'orphan' && <PcTag tone="warn">ไม่มีใบจ่าย</PcTag>}
                                  {d.state === 'partial' && <PcTag tone="warn">เหลือ {d.freeLines.length} บรรทัด {pcMoney(d.freeAmt)} ไม่มีใบ</PcTag>}
                                </td>
                                {canEdit && <td style={pcTd('center')} onClick={e => e.stopPropagation()}>
                                  {d.state === 'ignored'
                                    ? <button className="btn btn-ghost" style={{ padding: '2px 7px', fontSize: 11 }} onClick={() => setOfStatus(d, '')}>คืน</button>
                                    : (d.state === 'orphan' && <button className="btn btn-ghost" style={{ padding: '2px 7px', fontSize: 11 }} title="OF นี้ไม่เกี่ยวกับใบจ่ายในทะเบียน — ไม่ต้องติดตาม" onClick={() => setOfStatus(d, 'ignored')}>ซ่อน</button>)}
                                </td>}
                              </tr>
                              {open && d.lines.map(l => {
                                const owner = b.lineOwner[l.id] ? b.payById[b.lineOwner[l.id]] : null;
                                return (
                                  <tr key={l.id} style={{ background: 'var(--ink-50)' }}>
                                    <td style={pcTd()}></td><td style={pcTd('left', { color: 'var(--ink-500)', fontSize: 11 })}>บรรทัด {l.lineNo || '—'}</td>
                                    <td style={pcTd()}>{pcD(l.refDate)}</td>
                                    <td style={pcTd('left', { fontSize: 11.5 })} colSpan={1}>{l.itemName}{l.vendor ? <span style={{ color: 'var(--ink-500)' }}> · {l.vendor}</span> : null}</td>
                                    <td style={pcTd()}>{l.deptCode}</td><td style={pcTd('right')}>{pcMoney(l.amount)}</td><td style={pcTd()}></td>
                                    <td style={pcTd('left', { fontSize: 11.5 })}>
                                      {owner ? <button style={pcLinkBtn} onClick={() => canEdit && setLinkPay(owner)}>ใบ {owner.docNo} · {owner.requester} · {pcMoney(owner.net)}</button> : <span style={{ color: 'var(--warn)' }}>— ไม่มีใบจ่าย</span>}
                                    </td>
                                    {canEdit && <td style={pcTd()}></td>}
                                  </tr>
                                );
                              })}
                            </React.Fragment>
                          );
                        })}
                      </tbody>
                    </table>
                  </div>
                )}
              </div>
            )}

            {tab === 'dept' && (() => {
              const rows = pcDeptSummary(b, Y);
              const tot = ['n', 'total', 'back', 'wait', 'none'].reduce((o, k) => { o[k] = pcR2(rows.reduce((s, r) => s + r[k], 0)); return o; }, {});
              return (
                <div className="card" style={{ padding: 0, overflow: 'hidden', maxWidth: 860 }}>
                  <table style={{ width: '100%', borderCollapse: 'collapse' }}>
                    <thead><tr>
                      <th style={pcTh('left')}>ฝ่าย</th><th style={pcTh('right')}>ใบ</th><th style={pcTh('right')}>จ่ายออก</th>
                      <th style={pcTh('right')}>เงินเข้าแล้ว</th><th style={pcTh('right')}>ตั้ง OF แล้ว รอเงินเข้า</th><th style={pcTh('right')}>ยังไม่ตั้ง OF</th>
                    </tr></thead>
                    <tbody>
                      {rows.map(r => (
                        <tr key={r.dept}>
                          <td style={pcTd('left', { fontWeight: 700 })}>{r.dept}</td><td style={pcTd('right')}>{r.n}</td>
                          <td style={pcTd('right', { fontWeight: 700 })}>{pcMoney(r.total)}</td>
                          <td style={pcTd('right', { color: 'var(--good)' })}>{pcMoney(r.back)}</td>
                          <td style={pcTd('right', { color: r.wait ? 'var(--warn)' : 'var(--ink-400)' })}>{pcMoney(r.wait)}</td>
                          <td style={pcTd('right', { color: r.none ? 'var(--bad)' : 'var(--ink-400)', fontWeight: r.none ? 700 : 400 })}>{pcMoney(r.none)}</td>
                        </tr>
                      ))}
                    </tbody>
                    <tfoot><tr style={{ background: 'var(--ink-50)' }}>
                      <td style={pcTd('left', { fontWeight: 800 })}>รวม ปี {Y}</td><td style={pcTd('right', { fontWeight: 700 })}>{tot.n}</td>
                      <td style={pcTd('right', { fontWeight: 800 })}>{pcMoney(tot.total)}</td><td style={pcTd('right', { fontWeight: 700 })}>{pcMoney(tot.back)}</td>
                      <td style={pcTd('right', { fontWeight: 700 })}>{pcMoney(tot.wait)}</td><td style={pcTd('right', { fontWeight: 700 })}>{pcMoney(tot.none)}</td>
                    </tr></tfoot>
                  </table>
                </div>
              );
            })()}
          </>
        )}

        {payModal && (
          <PcPayModal edit={payModal.edit} b={b} rounds={b.rounds} deptOptions={deptOptions} whoOptions={whoOptions} whoDept={whoDept}
            canDelete={canDelete} onClose={() => setPayModal(null)} onSave={savePays} onDelete={deletePay} />
        )}
        {inModal && (
          <PcInModal edit={inModal.edit} preset={inModal.preset} b={b} canDelete={canDelete}
            onClose={() => setInModal(null)} onSave={saveIn} onDelete={deleteIn} />
        )}
        {linkPay && <PcLinkModal pay={b.payById[linkPay.id] || linkPay} b={b} onClose={() => setLinkPay(null)} onSave={(res) => saveLink(linkPay, res)} />}
        {importKind === 'of' && <PcOfImportModal existing={ofs} onClose={() => setImportKind(null)} onCommit={commitOf} />}
        {importKind === 'reg' && <PcRegImportModal txns={txns} onClose={() => setImportKind(null)} onCommit={commitRegister} />}
      </div>
    );
  }

  window.PettyCashPanel = PettyCashPanel;
  window.PCU = { build: pcBuild, equation: pcEquation, matchOf: pcMatchOf, propose: pcPropose, parseRegister: pcParseRegister, subset: pcSubset };
})();
