/* ══════════════════════════════════════════════════════════════════════════
 * 📅 นำเข้ารอบจ่ายจากบัญชี — ปุ่มบนหน้าเจ้าหนี้คงค้าง (#data_payables) · 2026-10-26
 *
 * บัญชีส่งรายการ "เลข AP ไหนเข้ารอบจ่ายวันไหน" มาเป็น Excel — คนละคนคนละหน้าตา
 *   (ของจริง ต.ค. 2026: "List รอบชำระสิ้นเดือนตุลาคม2569.xlsx" หัวแถว 3 = AP No. / Duedate /
 *    Net Amount / รอบส่ง · "List. Due 31-10-69.xlsx" หัวแถว 4 = AP Voucher No. / Due Date /
 *    Submission Date) → อ่านจากชื่อหัวตาราง ไม่ผูกตำแหน่งคอลัมน์
 *
 * ผลลัพธ์ = "แผนจ่าย" แบบเดียวกับปุ่มวางแผนจ่าย (forecastEntries EXPENSE_TYPE='AP',
 *   REF_DOC = เลข AP) → Bank Daily นับเป็นเงินออกของบัญชีที่เลือกเอง ไม่ต้องเพิ่มตาราง
 *   · ไม่แตะตาราง payables เลย (ปุ่ม "นำเข้า Excel" เดิมเป็นคนละเรื่อง — แทนข้อมูลเจ้าหนี้)
 *
 * กติกา (ผู้ใช้ตกลง 2026-10-26):
 *   · จับคู่ด้วยเลข AP อย่างเดียว (ชื่อเจ้าหนี้ในไฟล์สะกดไม่ตรงระบบ)
 *   · วันจ่าย = คอลัมน์ Due Date · วันส่งเอกสารเก็บเป็นหมายเหตุ · ยอด = ยอดในไฟล์
 *     (ไม่ตรงยอดเจ้าหนี้คงค้าง → ติดป้าย "ยอดไม่ตรง")
 *   · มีแผนอยู่แล้วคนละวัน/ยอด → ย้ายมาวันของรอบ (โชว์วันเดิม → วันใหม่ ติ๊กออกได้)
 *   · ยังไม่มีในเจ้าหนี้คงค้าง → ยังวางแผนให้ (Bank Daily เห็นเงิน) + ติดป้าย
 *   · จ่ายผ่าน PV แล้ว → ข้าม
 *   · นำเข้าฉบับแก้ซ้ำ → ใบเดิมไม่ถูกแตะ · ใบที่หายจากไฟล์ใหม่ = ถามก่อน (ค่าเริ่มไม่ติ๊ก
 *     เพราะอาจแค่นำเข้าไฟล์ของอีกคนไม่ครบ)
 *   · บัญชีจ่ายเริ่มต้น 4863 (เหมือนปุ่มวางแผนจ่ายที่ Bank Daily)
 *
 * แผนที่มาจากไฟล์มี NOTE ขึ้นต้น "รอบจ่ายบัญชี DD/MM/YYYY" — ใช้รู้ว่าแผนไหนมาจากรอบ
 *   (แถบสรุปรอบ / ป้าย "รอบบัญชี" ในตาราง) โดยไม่ต้องเพิ่มคอลัมน์
 * ชื่อทุกตัวขึ้นต้น apr* — ไฟล์ .jsx ทุกไฟล์แชร์ global scope เดียวกัน
 * ══════════════════════════════════════════════════════════════════════════ */

const APR_MARK = 'รอบจ่ายบัญชี';
const APR_DEFAULT_BANK_TAIL = '4863';
const APR_AP_RE = /^[A-Z]{2,4}-?\d{6,}$/;

const aprKey = (s) => String(s == null ? '' : s).trim().toUpperCase();
const aprIsActual = (f) => (f.ACTUAL_AMOUNT != null && f.ACTUAL_AMOUNT !== '') || f.STATUS === 'ACTUAL';
const aprIsoOf = (y, m, d) => {
  if (!(y > 1900 && m >= 1 && m <= 12 && d >= 1 && d <= 31)) return '';
  const dt = new Date(y, m - 1, d);
  if (dt.getMonth() !== m - 1) return '';
  return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
};
const aprToday = () => { const t = new Date(); return aprIsoOf(t.getFullYear(), t.getMonth() + 1, t.getDate()); };

/* ค่าในเซลล์ → ISO YYYY-MM-DD
 *   ตัวเลข = serial ของ Excel · ข้อความ D/M/YYYY (พ.ศ. ลบ 543 · ปี 2 หลัก ≥ 50 = พ.ศ. เช่น 69 → 2026) */
function aprDate(v) {
  if (v == null || v === '') return '';
  if (v instanceof Date && !isNaN(v)) return aprIsoOf(v.getFullYear(), v.getMonth() + 1, v.getDate());
  if (typeof v === 'number') {
    if (v < 20000 || v > 80000 || !window.XLSX) return '';
    const dc = window.XLSX.SSF.parse_date_code(v);
    return dc ? aprIsoOf(dc.y, dc.m, dc.d) : '';
  }
  const s = String(v).trim();
  let m = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
  if (m) return aprIsoOf(+m[1] > 2400 ? +m[1] - 543 : +m[1], +m[2], +m[3]);
  m = s.match(/^(\d{1,2})[\/.\-](\d{1,2})[\/.\-](\d{2,4})(?!\d)/);
  if (m) {
    let y = +m[3];
    if (m[3].length === 2) y = y >= 50 ? 2500 + y - 543 : 2000 + y;
    else if (y > 2400) y -= 543;
    return aprIsoOf(y, +m[2], +m[1]);
  }
  if (/^\d{5}(\.\d+)?$/.test(s)) return aprDate(Number(s));
  return '';
}
function aprNum(v) {
  if (typeof v === 'number') return isNaN(v) ? null : v;
  const s = String(v == null ? '' : v).replace(/[,฿\s]/g, '');
  if (!s || !/^-?\d+(\.\d+)?$/.test(s)) return null;
  return Number(s);
}
const aprR2 = (n) => Math.round((Number(n) || 0) * 100) / 100;
// วันที่ "ใดๆ" (ISO / DD/MM/YYYY) → ISO เพื่อเทียบวันเดียวกัน
const aprDay = (v) => aprDate(v) || '';
const aprFmt = (iso) => (iso ? fmtDate(iso) : '—');
const aprFmtShort = (iso) => (iso ? fmtDate(iso).slice(0, 5) : '—');

/* ── หัวตาราง: ชื่อคอลัมน์ (ตัดช่องว่าง/จุด/ตัวพิมพ์) → ช่องที่ต้องใช้ ── */
const aprNormHead = (s) => String(s == null ? '' : s).toLowerCase().replace(/[\s._:\-()/]+/g, '');
const APR_COLS = {
  ap:     h => h === 'apno' || h === 'apvoucherno' || h === 'apnumber' || h === 'vchno' || h.includes('เลขที่ap') || /^ap(voucher)?no/.test(h),
  pay:    h => h.includes('duedate') || h === 'due' || h.includes('วันจ่าย') || h.includes('รอบจ่าย') || h.includes('วันครบกำหนด') || h.includes('paydate') || h.includes('paymentdate'),
  amount: h => h.includes('netamount') || h.includes('netpayment') || h.includes('ยอดจ่าย') || h.includes('ยอดสุทธิ') || h === 'amount' || h.includes('จำนวนเงิน'),
  sent:   h => h.includes('รอบส่ง') || h.includes('submission') || h.includes('วันส่ง') || h.includes('วันที่ส่ง'),
  vendor: h => h.includes('supplier') || h.includes('vendor') || h.includes('เจ้าหนี้') || h.includes('ผู้ขาย') || h.includes('payee'),
  ref:    h => h.includes('refproj') || h.includes('dept') || h.includes('แผนก') || h.includes('โครงการ'),
  note:   h => h.includes('หมายเหตุ') || h.includes('remark'),
};
function aprFindHeader(aoa) {
  const lim = Math.min(aoa.length, 25);
  for (let r = 0; r < lim; r++) {
    const row = aoa[r] || [];
    const cols = {};
    row.forEach((c, i) => {
      const h = aprNormHead(c);
      if (!h) return;
      Object.keys(APR_COLS).forEach(k => { if (cols[k] == null && APR_COLS[k](h)) cols[k] = i; });
    });
    if (cols.ap != null && cols.amount != null) return { row: r, cols, labels: row };
  }
  return null;
}
// วันรอบจ่ายจากหัวไฟล์ / ชื่อชีต — ใช้เมื่อแถวไม่มีวันจ่ายของตัวเอง ("รอบจ่าย 31/10/2026" · ชีต "31102026")
function aprFallbackPayDate(aoa, headerRow, sheetName) {
  for (let r = 0; r < headerRow; r++) {
    for (const c of (aoa[r] || [])) {
      const m = String(c == null ? '' : c).match(/(?:รอบจ่าย|due)\D{0,6}(\d{1,2}[\/.\-]\d{1,2}[\/.\-]\d{2,4})/i);
      if (m) { const iso = aprDate(m[1]); if (iso) return iso; }
    }
  }
  const m = String(sheetName || '').match(/^(\d{2})(\d{2})(\d{4})$/);
  return m ? aprIsoOf(+m[3] > 2400 ? +m[3] - 543 : +m[3], +m[2], +m[1]) : '';
}

/* อ่านไฟล์หนึ่งไฟล์ (ทุกชีตที่มีหัวตารางครบ) → { name, rows:[{ap,amount,payIso,sentIso,vendor,ref,note}], noDate, error } */
function aprParseWorkbook(wb, fileName) {
  const out = { name: fileName, rows: [], noDate: 0, sheets: [], error: '' };
  (wb.SheetNames || []).forEach(sn => {
    const ws = wb.Sheets[sn];
    if (!ws) return;
    const aoa = window.XLSX.utils.sheet_to_json(ws, { header: 1, raw: true, defval: '' });
    const hd = aprFindHeader(aoa);
    if (!hd) return;
    const fbPay = aprFallbackPayDate(aoa, hd.row, sn);
    const c = hd.cols;
    const cell = (row, k) => (c[k] == null ? '' : row[c[k]]);
    let n = 0;
    for (let r = hd.row + 1; r < aoa.length; r++) {
      const row = aoa[r] || [];
      const ap = aprKey(cell(row, 'ap'));
      if (!APR_AP_RE.test(ap)) continue;            // แถวสรุป/แถวว่าง/หัวซ้ำ
      const amount = aprNum(cell(row, 'amount'));
      if (amount == null) continue;
      const payIso = aprDate(cell(row, 'pay')) || fbPay;
      if (!payIso) { out.noDate++; continue; }
      out.rows.push({
        ap, amount: aprR2(Math.abs(amount)), payIso,
        sentIso: aprDate(cell(row, 'sent')),
        vendor: String(cell(row, 'vendor') || '').trim(),
        ref: String(cell(row, 'ref') || '').trim(),
        note: String(cell(row, 'note') || '').trim(),
        file: fileName,
      });
      n++;
    }
    if (n) out.sheets.push(sn);
  });
  if (!out.rows.length && !out.noDate) {
    out.error = 'ไม่เจอหัวตารางที่มีคอลัมน์เลขที่ AP + ยอดเงิน (เช่น "AP No." / "AP Voucher No." กับ "Net Amount")';
  }
  return out;
}
function aprReadFile(file) {
  return new Promise((resolve) => {
    if (!window.XLSX) { resolve({ name: file.name, rows: [], noDate: 0, sheets: [], error: 'ไม่พบไลบรารีอ่าน Excel' }); return; }
    const reader = new FileReader();
    reader.onload = (e) => {
      try {
        const wb = window.XLSX.read(e.target.result, { type: 'array', cellDates: false });
        resolve(aprParseWorkbook(wb, file.name));
      } catch (err) {
        resolve({ name: file.name, rows: [], noDate: 0, sheets: [], error: 'อ่านไฟล์ไม่ได้: ' + err.message });
      }
    };
    reader.onerror = () => resolve({ name: file.name, rows: [], noDate: 0, sheets: [], error: 'อ่านไฟล์ไม่ได้' });
    reader.readAsArrayBuffer(file);
  });
}

/* แผนที่มาจากรอบจ่ายบัญชี → วันรอบ (ISO) หรือ '' · นับเฉพาะที่วันจ่ายยังตรงกับวันรอบ
 *   (ถ้าผู้ใช้เลื่อนเองทีหลัง NOTE ยังอยู่ แต่ไม่ถือว่าเป็นของรอบแล้ว) */
function aprRoundOf(f) {
  const m = String((f && f.NOTE) || '').match(/^รอบจ่ายบัญชี (\d{2}\/\d{2}\/\d{4})/);
  if (!m) return '';
  const iso = aprDate(m[1]);
  return iso && iso === aprDay(f.PAYMENT_DATE) ? iso : '';
}

/* ── จัดประเภทรายการในไฟล์เทียบกับระบบ ─────────────────────────────────────
 *   kind: new (วางแผนใหม่) · move (มีแผนเดิมคนละวัน/ยอด → ย้าย) · same (เหมือนเดิม ไม่ทำอะไร)
 *         paid (จ่ายผ่าน PV แล้ว → ข้าม) · drop (อยู่ในรอบเดียวกันจากครั้งก่อน แต่ไม่มีในไฟล์ชุดนี้)
 *   flags: nofound (ไม่มีในเจ้าหนี้คงค้าง) · diff (ยอดไม่ตรง) · dup (เลขซ้ำในไฟล์ ยอด/วันต่างกัน) */
function aprClassify(files, data) {
  const payables = data.payables || [], fe = data.forecastEntries || [], pv = data.pvVouchers || [];
  const apByNo = new Map();
  payables.forEach(p => { const k = aprKey(p.vchno); if (k && !apByNo.has(k)) apByNo.set(k, p); });
  const paidSet = new Set();
  pv.forEach(p => { const k = aprKey(p.AP_No); if (k) paidSet.add(k); });
  const plansByRef = new Map();
  fe.forEach(f => {
    if (String(f.EXPENSE_TYPE || '').toUpperCase() !== 'AP') return;
    const k = aprKey(f.REF_DOC);
    if (!k) return;
    if (!plansByRef.has(k)) plansByRef.set(k, []);
    plansByRef.get(k).push(f);
  });

  const items = [], seen = new Map();
  let dupSame = 0;
  files.forEach(fl => (fl.rows || []).forEach(r => {
    const prev = seen.get(r.ap);
    if (prev) {
      if (prev.payIso === r.payIso && Math.abs(prev.amount - r.amount) < 0.01) dupSame++;
      else prev.dup = (prev.dup || []).concat([{ file: r.file, payIso: r.payIso, amount: r.amount }]);
      return;
    }
    const ap = apByNo.get(r.ap) || null;
    const plans = plansByRef.get(r.ap) || [];
    const open = plans.filter(f => !aprIsActual(f))
      .sort((a, b) => (aprDay(a.PAYMENT_DATE) < aprDay(b.PAYMENT_DATE) ? -1 : 1));
    const net = ap ? aprR2(parseNum(ap.netpayment)) : null;
    const it = {
      key: r.ap, ap: r.ap, amount: r.amount, payIso: r.payIso, sentIso: r.sentIso, file: r.file,
      vendor: (ap && String(ap.cust_name || '').trim()) || r.vendor, fileVendor: r.vendor, ref: r.ref,
      payable: ap, net, open, replaceIds: [], fromDates: [],
      nofound: !ap, diff: !!ap && Math.abs(net - r.amount) > 1,
      category: (ap && ap.cf_category) || (open[0] && open[0].CATEGORY) || null,
    };
    if (paidSet.has(r.ap)) it.kind = 'paid';
    else if (!open.length) it.kind = 'new';
    else {
      const sum = aprR2(open.reduce((s, f) => s + Math.abs(parseNum(f.AMOUNT)), 0));
      const allSameDay = open.every(f => aprDay(f.PAYMENT_DATE) === r.payIso);
      // แบ่งงวดไว้ + ไฟล์จ่ายบางส่วน และมีงวดที่วัน/ยอดตรงกับไฟล์อยู่แล้ว (นำเข้าซ้ำ) = เหมือนเดิม
      const exact = (open.length > 1 && r.amount < sum - 1)
        ? open.find(f => aprDay(f.PAYMENT_DATE) === r.payIso && Math.abs(Math.abs(parseNum(f.AMOUNT)) - r.amount) < 0.01)
        : null;
      if ((allSameDay && Math.abs(sum - r.amount) < 0.01) || exact) {
        it.kind = 'same';
        // แผนเดิมที่วางเองไว้วันเดียวกัน — ติดป้ายรอบให้ (แถบสรุปรอบนับครบ) + ใส่บัญชีถ้ายังว่าง
        //   (Bank Daily ไม่นับแผนที่ไม่มีบัญชี)
        const mine = exact ? [exact] : open;
        it.stampIds = mine.filter(f => aprRoundOf(f) !== r.payIso || !String(f.Bank_AC || '').trim()).map(f => f.id);
        it.noBank = mine.some(f => !String(f.Bank_AC || '').trim());
      } else {
        it.kind = 'move';
        // แบ่งงวดไว้หลายงวด + ไฟล์จ่ายแค่บางส่วน → ย้ายเฉพาะงวดที่ยอดใกล้สุด ที่เหลือคงไว้
        let targets = open;
        if (open.length > 1 && r.amount < sum - 1) {
          let best = open[0];
          open.forEach(f => { if (Math.abs(Math.abs(parseNum(f.AMOUNT)) - r.amount) < Math.abs(Math.abs(parseNum(best.AMOUNT)) - r.amount)) best = f; });
          targets = [best];
        }
        it.replaceIds = targets.map(f => f.id);
        it.fromDates = [...new Set(targets.map(f => aprDay(f.PAYMENT_DATE)).filter(Boolean))];
        it.fromAmount = aprR2(targets.reduce((s, f) => s + Math.abs(parseNum(f.AMOUNT)), 0));
      }
    }
    seen.set(r.ap, it);
    items.push(it);
  }));

  // ใบที่อยู่ในรอบเดียวกันจากการนำเข้าครั้งก่อน แต่ไม่มีในไฟล์ชุดนี้
  const payDays = new Set(items.map(i => i.payIso));
  const dropped = [];
  fe.forEach(f => {
    if (aprIsActual(f)) return;
    const rd = aprRoundOf(f);
    if (!rd || !payDays.has(rd) || seen.has(aprKey(f.REF_DOC)) || paidSet.has(aprKey(f.REF_DOC))) return;
    const ap = apByNo.get(aprKey(f.REF_DOC)) || null;
    dropped.push({
      key: 'drop:' + f.id, kind: 'drop', ap: aprKey(f.REF_DOC), planId: f.id,
      amount: aprR2(Math.abs(parseNum(f.AMOUNT))), payIso: rd,
      vendor: (ap && ap.cust_name) || String(f.DESCRIPTION || '').replace(/^จ่าย\s*/, '').replace(/\s*\([^)]*\)\s*$/, ''),
      nofound: !ap, note: f.NOTE || '',
    });
  });
  return { items, dropped, dupSame };
}

// บัญชีธนาคารที่ยังใช้งาน (ไม่ปิด/พักบัญชี) + ค่าเริ่ม = บัญชีลงท้าย 4863
function aprBanks(bankAccounts) {
  return (bankAccounts || [])
    .filter(b => { const t = String(b.accountType || b.account_type || '').toLowerCase(); return t !== 'closed' && t !== 'dormant'; })
    .map(b => ({ no: String(hpBankAcNo(b) || '').trim(), name: hpBankName(b) }))
    .filter(b => b.no);
}
const aprDefaultBank = (banks) => {
  const hit = banks.find(b => b.no.replace(/\D/g, '').slice(-4) === APR_DEFAULT_BANK_TAIL);
  return hit ? hit.no : '';
};

/* ข้อมูลจริงจาก server มาครบหรือยัง — ตาราง forecastEntries/payables ช่วงแรกหลังรีเฟรชอาจว่าง
 *   ถ้าจัดประเภทตอนนั้น ทุกใบจะกลายเป็น "วางแผนใหม่" = แผนซ้ำกับของเดิม */
function aprServerReady() {
  try {
    const info = (window.WTPData && WTPData._autoPushInfo) ? WTPData._autoPushInfo() : null;
    if (info && typeof info.serverDataLoaded === 'boolean') return info.serverDataLoaded;
  } catch (_) {}
  return true;
}
function aprUseServerReady() {
  const [ready, setReady] = React.useState(aprServerReady);
  React.useEffect(() => {
    if (ready) return;
    const t = setInterval(() => { if (aprServerReady()) { setReady(true); clearInterval(t); } }, 400);
    return () => clearInterval(t);
  }, [ready]);
  return ready;
}

const APR_KIND = {
  new:  { label: 'วางแผนใหม่',  color: 'var(--good)',          bg: 'color-mix(in oklch, var(--good) 12%, transparent)' },
  move: { label: 'ย้ายวันจ่าย', color: 'var(--brand-700)',     bg: 'var(--brand-50)' },
  same: { label: 'เหมือนเดิม',   color: 'var(--ink-500)',       bg: 'var(--ink-50)' },
  paid: { label: 'จ่ายแล้ว (มี PV)', color: 'var(--ink-400)',   bg: 'var(--ink-50)' },
  drop: { label: 'หายจากไฟล์',  color: 'var(--bad)',           bg: 'color-mix(in oklch, var(--bad) 9%, transparent)' },
};
const APR_WARN = { color: 'oklch(50% 0.13 70)', bg: 'color-mix(in oklch, oklch(62% 0.15 75) 14%, transparent)' };
const aprChip = (text, c, title) => (
  <span title={title} style={{ display: 'inline-block', fontSize: 10.5, fontWeight: 700, color: c.color, background: c.bg, borderRadius: 5, padding: '1px 6px', whiteSpace: 'nowrap' }}>{text}</span>
);

/* ── หน้าต่างนำเข้า ───────────────────────────────────────────────────────── */
function ApPayRoundModal({ data, setData, toast, onClose }) {
  const [files, setFiles]       = React.useState([]);   // ผล aprParseWorkbook ต่อไฟล์
  const [reading, setReading]   = React.useState(false);
  const [dragOver, setDragOver] = React.useState(false);
  const banks = React.useMemo(() => aprBanks(data.bankAccounts), [data.bankAccounts]);
  const [bankAc, setBankAc]     = React.useState(() => aprDefaultBank(aprBanks(data.bankAccounts)));
  const [filter, setFilter]     = React.useState('all');
  const [off, setOff]           = React.useState(() => new Set());   // ใบที่ผู้ใช้ติ๊กออก (new/move)
  const [dropOn, setDropOn]     = React.useState(() => new Set());   // ใบที่หายจากไฟล์ ที่ผู้ใช้ติ๊กให้ยกเลิกแผน
  const [busy, setBusy]         = React.useState(false);
  const inputRef = React.useRef(null);
  const serverReady = aprUseServerReady();

  // ตั้งบัญชีเริ่มต้นเมื่อรายชื่อบัญชีโหลดมาทีหลัง
  React.useEffect(() => { if (!bankAc && banks.length) setBankAc(aprDefaultBank(banks)); }, [banks]);

  const addFiles = async (list) => {
    const arr = [...(list || [])].filter(f => /\.(xlsx|xlsm|xls)$/i.test(f.name));
    if (!arr.length) { toast && toast('เลือกไฟล์ Excel (.xlsx)'); return; }
    setReading(true);
    const parsed = await Promise.all(arr.map(aprReadFile));
    setReading(false);
    // ไฟล์ชื่อเดิม = แทนของเดิม (เลือกไฟล์ฉบับแก้ซ้ำ)
    setFiles(prev => [...prev.filter(p => !parsed.some(n => n.name === p.name)), ...parsed]);
  };
  const removeFile = (name) => setFiles(prev => prev.filter(f => f.name !== name));

  const okFiles = files.filter(f => f.rows.length);
  const cls = React.useMemo(
    () => aprClassify(okFiles, data),
    [files, data.payables, data.forecastEntries, data.pvVouchers]
  );
  const all = React.useMemo(() => [...cls.items, ...cls.dropped], [cls]);
  const actOf = (it) => it.kind === 'drop' ? dropOn.has(it.key)
    : (it.kind === 'new' || it.kind === 'move') && !off.has(it.key);
  const toggle = (it) => {
    if (it.kind === 'drop') setDropOn(s => { const n = new Set(s); n.has(it.key) ? n.delete(it.key) : n.add(it.key); return n; });
    else if (it.kind === 'new' || it.kind === 'move') setOff(s => { const n = new Set(s); n.has(it.key) ? n.delete(it.key) : n.add(it.key); return n; });
  };

  const cnt = { new: 0, move: 0, same: 0, paid: 0, drop: cls.dropped.length, nofound: 0, diff: 0, dup: 0 };
  cls.items.forEach(it => { cnt[it.kind]++; if (it.nofound) cnt.nofound++; if (it.diff) cnt.diff++; if (it.dup) cnt.dup++; });
  const willPlan = cls.items.filter(actOf);
  const willDrop = cls.dropped.filter(actOf);
  const willStamp = cls.items.filter(it => it.kind === 'same' && it.stampIds && it.stampIds.length);
  const hasWork = willPlan.length > 0 || willDrop.length > 0 || willStamp.length > 0;
  const planSum = aprR2(willPlan.reduce((s, it) => s + it.amount, 0));
  const payDays = [...new Set(cls.items.map(i => i.payIso))].sort();

  const shown = all.filter(it =>
    filter === 'all' ? true
    : filter === 'nofound' ? !!it.nofound && it.kind !== 'drop'
    : filter === 'diff' ? !!it.diff
    : filter === 'dup' ? !!it.dup
    : it.kind === filter);

  const commit = () => {
    if (busy) return;
    if (!serverReady) { toast && toast('ข้อมูลเดิมยังโหลดไม่เสร็จ — รอสักครู่แล้วกดใหม่'); return; }
    if (!hasWork) { toast && toast('ไม่มีรายการที่ต้องบันทึก'); return; }
    if ((willPlan.length || willStamp.some(it => it.noBank)) && !bankAc) { toast && toast('เลือกบัญชีที่จ่ายก่อน — Bank Daily นับแผนจ่ายตามบัญชี'); return; }
    if (willDrop.length && !window.confirm('ยกเลิกแผนจ่ายของ ' + willDrop.length + ' ใบที่ไม่มีในไฟล์ชุดนี้?')) return;
    setBusy(true);
    const today = aprToday(), ts = Date.now();
    const feById = new Map((data.forecastEntries || []).map(f => [f.id, f]));
    // หมายเหตุเดิมที่ผู้ใช้พิมพ์ไว้ (ไม่ใช่ป้ายรอบเก่า) ต่อท้ายไว้ ไม่ทิ้ง
    const keepNote = (id) => { const n = String((feById.get(id) || {}).NOTE || '').trim(); return n && !n.startsWith(APR_MARK) ? ' · ' + n : ''; };
    const noteOf = (it, id) => APR_MARK + ' ' + fmtDate(it.payIso)
      + (it.sentIso ? ' · ส่ง ' + fmtDate(it.sentIso) : '') + ' · ' + it.file + (id ? keepNote(id) : '');
    const descOf = (it) => 'จ่าย ' + (it.vendor || '') + ' (' + it.ap + ')';
    // ย้าย = แก้แถวเดิมแถวแรกในที่ (คง id) + ลบแถวที่เหลือของกลุ่มเดียวกัน
    const patchById = new Map(), killIds = new Set(willDrop.map(d => d.planId));
    const adds = [];
    willStamp.forEach(it => it.stampIds.forEach(id => {
      const f = feById.get(id) || {};
      patchById.set(id, { NOTE: noteOf(it, id), Bank_AC: String(f.Bank_AC || '').trim() || bankAc || null });
    }));
    willPlan.forEach((it, i) => {
      const fields = {
        PAYMENT_DATE: it.payIso, AMOUNT: String(-Math.abs(it.amount)), Bank_AC: bankAc || null,
        CATEGORY: it.category || null, NOTE: noteOf(it), DESCRIPTION: descOf(it), STATUS: 'PLANNED',
      };
      if (it.kind === 'move' && it.replaceIds.length) {
        patchById.set(it.replaceIds[0], { ...fields, NOTE: noteOf(it, it.replaceIds[0]) });
        it.replaceIds.slice(1).forEach(id => killIds.add(id));
      } else {
        adds.push({
          id: 'ap-' + ts + '-r' + i, DATE: today, EXPENSE_TYPE: 'AP', JOB_NO: null, PROJECT_NAME: null,
          IS_ACCRUED: null, ACTUAL_AMOUNT: null, ACTUAL_DATE: null, REF_DOC: it.ap, BOOKED_AT: null, CFS_ACTIVITY: null,
          ...fields,
        });
      }
    });
    setData(prev => {
      const fe = (prev.forecastEntries || [])
        .filter(f => !killIds.has(f.id) || aprIsActual(f))
        .map(f => (patchById.has(f.id) && !aprIsActual(f)) ? { ...f, ...patchById.get(f.id) } : f);
      return { ...prev, forecastEntries: [...fe, ...adds] };
    });
    if (window.WTPData && typeof window.WTPData.forceSyncNow === 'function') window.WTPData.forceSyncNow();
    const parts = [];
    const nNew = willPlan.filter(i => i.kind === 'new').length, nMove = willPlan.length - nNew;
    if (nNew) parts.push('วางแผนใหม่ ' + nNew);
    if (nMove) parts.push('ย้ายวัน ' + nMove);
    if (willDrop.length) parts.push('ยกเลิกแผน ' + willDrop.length);
    if (!parts.length) parts.push('ติดป้ายรอบให้แผนเดิม ' + willStamp.length);
    toast && toast('รอบจ่าย ' + payDays.map(aprFmt).join(', ') + ' · ' + parts.join(' · ') + ' ใบ'
      + (willPlan.length || willStamp.length ? ' · ยอดไปที่ Bank Daily แล้ว' : ''));
    onClose();
  };

  const tab = (k, label, n, c) => (n > 0 || k === 'all') && (
    <button key={k} onClick={() => setFilter(filter === k ? 'all' : k)}
      style={{ border: '1px solid ' + (filter === k ? c.color : 'transparent'), background: c.bg, color: c.color,
        borderRadius: 6, padding: '3px 9px', fontSize: 11.5, fontWeight: 700, cursor: 'pointer', whiteSpace: 'nowrap' }}>
      {label} {k !== 'all' && n}
    </button>
  );

  return (
    <Modal open maxWidth={860} onClose={onClose}
      title={<span>📅 นำเข้ารอบจ่ายจากบัญชี</span>}
      footer={<>
        <span style={{ marginRight: 'auto', fontSize: 12, color: 'var(--ink-500)' }}>
          {okFiles.length > 0 && <>ไม่แตะข้อมูลเจ้าหนี้คงค้าง — สร้าง/ย้าย <strong>แผนจ่าย</strong> ที่ Bank Daily ใช้เท่านั้น</>}
        </span>
        <button className="btn btn-ghost" onClick={onClose}>ยกเลิก</button>
        <button className="btn btn-primary" onClick={commit}
          disabled={busy || !serverReady || !hasWork}
          title={!serverReady ? 'ข้อมูลเดิมยังโหลดไม่เสร็จ — รอสักครู่' : undefined}>
          <Icon name="check" size={13} />
          {willPlan.length
            ? ` วางแผนจ่าย ${willPlan.length} ใบ · ${payDays.map(aprFmtShort).join(', ')}`
            : willDrop.length ? ` ยกเลิกแผน ${willDrop.length} ใบ` : ' บันทึก'}
        </button>
      </>}>
      <div style={{ display: 'grid', gap: 12 }}>
        {!serverReady && (
          <div style={{ padding: '8px 12px', borderRadius: 8, fontSize: 12, background: APR_WARN.bg, color: APR_WARN.color }}>
            ⏳ <strong>กำลังโหลดข้อมูลเดิมจากเซิร์ฟเวอร์…</strong> ยังเทียบกับแผนจ่ายเดิมไม่ได้ — ปุ่มบันทึกจะเปิดเมื่อโหลดเสร็จ
          </div>
        )}

        {/* ที่วางไฟล์ — ลากได้หลายไฟล์พร้อมกัน */}
        <div onClick={() => inputRef.current && inputRef.current.click()}
          onDragOver={e => { e.preventDefault(); setDragOver(true); }}
          onDragLeave={() => setDragOver(false)}
          onDrop={e => { e.preventDefault(); setDragOver(false); addFiles(e.dataTransfer.files); }}
          style={{ border: '2px dashed ' + (dragOver ? 'var(--brand-500)' : 'var(--ink-150)'), borderRadius: 10,
            padding: okFiles.length ? '10px 14px' : '22px 14px', textAlign: 'center', cursor: 'pointer',
            background: dragOver ? 'var(--brand-50)' : 'var(--panel)', fontSize: 12.5, color: 'var(--ink-600)' }}>
          <input ref={inputRef} type="file" accept=".xlsx,.xlsm,.xls" multiple style={{ display: 'none' }}
            onChange={e => { addFiles(e.target.files); e.target.value = ''; }} />
          {reading ? 'กำลังอ่านไฟล์…'
            : okFiles.length ? '＋ เพิ่มไฟล์อีก (ลากมาวาง หรือกดเลือก)'
            : <><div style={{ fontSize: 22, marginBottom: 4 }}>📂</div>
                <strong>ลากไฟล์รายการรอบจ่ายจากบัญชีมาวาง</strong> หรือกดเพื่อเลือก — ใส่ทีละหลายไฟล์ได้<br />
                <span style={{ fontSize: 11.5, color: 'var(--ink-400)' }}>ต้องมีคอลัมน์เลขที่ AP · ยอดเงิน · Due Date (หน้าตาไฟล์ต่างกันได้ ระบบหาหัวตารางเอง)</span></>}
        </div>

        {/* การ์ดต่อไฟล์ */}
        {files.length > 0 && (
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: 8 }}>
            {files.map(f => {
              const sum = aprR2(f.rows.reduce((s, r) => s + r.amount, 0));
              const pays = [...new Set(f.rows.map(r => r.payIso))].sort();
              const sents = [...new Set(f.rows.map(r => r.sentIso).filter(Boolean))].sort();
              const types = {};
              f.rows.forEach(r => { const t = r.ap.slice(0, 3); types[t] = (types[t] || 0) + 1; });
              return (
                <div key={f.name} style={{ position: 'relative', padding: '9px 30px 9px 12px', borderRadius: 8,
                  background: f.error ? 'color-mix(in oklch, var(--bad) 7%, transparent)' : 'var(--ink-50)' }}>
                  <button onClick={() => removeFile(f.name)} title="เอาไฟล์นี้ออก"
                    style={{ position: 'absolute', top: 6, right: 6, border: 'none', background: 'none', cursor: 'pointer', color: 'var(--ink-400)', fontSize: 14 }}>×</button>
                  <div style={{ fontSize: 11.5, color: 'var(--ink-500)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={f.name}>📄 {f.name}</div>
                  {f.error
                    ? <div style={{ fontSize: 12, color: 'var(--bad)', marginTop: 3 }}>{f.error}</div>
                    : <>
                        <div style={{ fontSize: 13, marginTop: 3, color: 'var(--ink-800)' }}>
                          {f.rows.length} ใบ · <strong style={{ fontVariantNumeric: 'tabular-nums' }}>{fmtNum(sum, 2)}</strong>
                        </div>
                        <div style={{ fontSize: 11.5, color: 'var(--ink-500)', marginTop: 1 }}>
                          จ่าย {pays.map(aprFmt).join(', ')}{sents.length ? ' · ส่ง ' + sents.map(aprFmtShort).join(', ') : ''}
                          {' · '}{Object.keys(types).sort().map(t => t + ' ' + types[t]).join(' · ')}
                        </div>
                        {f.noDate > 0 && <div style={{ fontSize: 11.5, color: APR_WARN.color, marginTop: 2 }}>⚠ ข้าม {f.noDate} แถวที่ไม่มีวันจ่าย</div>}
                      </>}
                </div>
              );
            })}
          </div>
        )}

        {okFiles.length > 0 && (<>
          <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8, fontSize: 12.5 }}>
            <span style={{ color: 'var(--ink-600)' }}>จ่ายจากบัญชี</span>
            <select value={bankAc} onChange={e => setBankAc(e.target.value)}
              style={{ height: 32, fontSize: 12.5, padding: '0 8px', border: '1px solid ' + (bankAc ? 'var(--ink-150)' : 'var(--bad)'), borderRadius: 7, background: 'var(--panel)', color: 'var(--ink-800)', minWidth: 230 }}>
              <option value="">— เลือกบัญชี —</option>
              {banks.map(b => <option key={b.no} value={b.no}>{b.name} · {b.no}</option>)}
            </select>
            <span style={{ fontSize: 11.5, color: 'var(--ink-400)' }}>ใช้กับทุกใบที่วางแผน/ย้ายวันในรอบนี้</span>
          </div>

          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center' }}>
            {tab('all', 'ทั้งหมด ' + all.length, all.length, { color: 'var(--ink-700)', bg: 'var(--ink-50)' })}
            {tab('new', 'วางแผนใหม่', cnt.new, APR_KIND.new)}
            {tab('move', 'ย้ายวันจ่าย', cnt.move, APR_KIND.move)}
            {tab('same', 'เหมือนเดิม', cnt.same, APR_KIND.same)}
            {tab('diff', 'ยอดไม่ตรง', cnt.diff, APR_WARN)}
            {tab('nofound', 'ไม่มีในเจ้าหนี้คงค้าง', cnt.nofound, APR_WARN)}
            {tab('dup', 'เลขซ้ำในไฟล์', cnt.dup, APR_WARN)}
            {tab('paid', 'จ่ายแล้ว ข้าม', cnt.paid, APR_KIND.paid)}
            {tab('drop', 'หายจากไฟล์', cnt.drop, APR_KIND.drop)}
            <span style={{ marginLeft: 'auto', fontSize: 12.5, color: 'var(--ink-600)' }}>
              จะวางแผน <strong style={{ color: 'var(--bad)', fontVariantNumeric: 'tabular-nums' }}>{fmtNum(planSum, 2)}</strong>
            </span>
          </div>

          {cnt.drop > 0 && filter !== 'drop' && (
            <div style={{ fontSize: 11.5, color: 'var(--ink-500)' }}>
              มี {cnt.drop} ใบที่อยู่ในรอบ {payDays.map(aprFmtShort).join(', ')} จากการนำเข้าครั้งก่อน แต่ไม่มีในไฟล์ชุดนี้ —{' '}
              <a href="#" onClick={e => { e.preventDefault(); setFilter('drop'); }} style={{ color: 'var(--brand-700)' }}>ดูแล้วติ๊กใบที่จะยกเลิกแผน</a>
              {' '}(ถ้านำเข้าแค่ไฟล์ของคนเดียว ปล่อยไว้ไม่ต้องติ๊ก)
            </div>
          )}

          <div style={{ maxHeight: 'min(360px, calc(100vh - 460px))', minHeight: 140, overflowY: 'auto', border: '1px solid var(--ink-100)', borderRadius: 8 }}>
            <table className="tbl" style={{ width: '100%', fontSize: 12 }}>
              <thead style={{ position: 'sticky', top: 0, zIndex: 2, background: 'var(--surface)' }}>
                <tr>
                  <th style={{ width: 30 }} />
                  <th style={{ width: 128, textAlign: 'left' }}>เลขที่ AP</th>
                  <th style={{ textAlign: 'left' }}>เจ้าหนี้</th>
                  <th style={{ width: 112, textAlign: 'right' }}>ยอดในไฟล์</th>
                  <th style={{ width: 200, textAlign: 'left' }}>ผล</th>
                </tr>
              </thead>
              <tbody>
                {shown.length === 0 && <tr><td colSpan={5} style={{ padding: 24, textAlign: 'center' }} className="muted">ไม่มีรายการ</td></tr>}
                {shown.map(it => {
                  const k = APR_KIND[it.kind];
                  const can = it.kind === 'new' || it.kind === 'move' || it.kind === 'drop';
                  const on = actOf(it);
                  return (
                    <tr key={it.key} onClick={() => can && toggle(it)} style={{ cursor: can ? 'pointer' : 'default', opacity: can && !on ? 0.55 : 1 }}>
                      <td style={{ textAlign: 'center' }}>
                        {can ? <input type="checkbox" checked={on} onChange={() => toggle(it)} onClick={e => e.stopPropagation()} style={{ cursor: 'pointer' }} /> : null}
                      </td>
                      <td style={{ fontFamily: 'ui-monospace', color: 'var(--brand-700)', fontWeight: 600 }}>{it.ap}</td>
                      <td style={{ color: 'var(--ink-700)', maxWidth: 260, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={it.vendor}>
                        {it.vendor || '—'}
                        {it.nofound && <> {aprChip('ไม่มีในเจ้าหนี้คงค้าง', APR_WARN, 'ยังวางแผนให้ — พอนำเข้าเจ้าหนี้คงค้างที่มีเลขนี้ จะผูกกันเอง')}</>}
                      </td>
                      <td style={{ textAlign: 'right', fontVariantNumeric: 'tabular-nums', fontWeight: 700, color: 'var(--ink-800)' }}>{fmtNum(it.amount, 2)}</td>
                      <td>
                        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
                          {it.kind === 'new'  && aprChip('ใหม่ → ' + aprFmtShort(it.payIso), k)}
                          {it.kind === 'move' && aprChip((it.fromDates.length ? it.fromDates.map(aprFmtShort).join(', ') : 'เดิม') + ' → ' + aprFmtShort(it.payIso)
                            + (Math.abs((it.fromAmount || 0) - it.amount) > 0.01 ? ' · ยอดเดิม ' + fmtNum(it.fromAmount, 0) : ''), k)}
                          {it.kind === 'same' && aprChip(it.noBank ? 'มีแผนแล้ว · ใส่บัญชีให้' : 'มีแผน ' + aprFmtShort(it.payIso) + ' อยู่แล้ว', k,
                            it.noBank ? 'แผนเดิมวันเดียวกันยังไม่ระบุบัญชี (Bank Daily ไม่นับ) — จะใส่บัญชีที่เลือกให้' : undefined)}
                          {it.kind === 'paid' && aprChip('จ่ายผ่าน PV แล้ว · ข้าม', k)}
                          {it.kind === 'drop' && aprChip('ไม่มีในไฟล์ชุดนี้ · ติ๊ก = ยกเลิกแผน', k)}
                          {it.diff && aprChip('ในระบบ ' + fmtNum(it.net, 2), APR_WARN, 'ยอดในเจ้าหนี้คงค้างไม่ตรงกับไฟล์ — วางแผนตามยอดในไฟล์')}
                          {it.dup && aprChip('เลขซ้ำ: ' + it.dup.map(d => aprFmtShort(d.payIso) + ' ' + fmtNum(d.amount, 0)).join(', '), APR_WARN, 'เลขเดียวกันอยู่หลายไฟล์/หลายแถว แต่วันหรือยอดต่างกัน — ใช้แถวแรก (' + it.file + ')')}
                        </div>
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
          {cls.dupSame > 0 && <div style={{ fontSize: 11.5, color: 'var(--ink-400)' }}>ข้ามแถวที่ซ้ำกันทุกอย่าง {cls.dupSame} แถว</div>}
        </>)}
      </div>
    </Modal>
  );
}

/* ── แถบสรุปรอบจ่าย (หน้าเจ้าหนี้คงค้าง) — โชว์รอบที่ยังไม่จ่าย ย้อนหลังไม่เกิน 14 วัน ──
 *   rounds = [{ iso, refs:Set, count, sum, nofound:[{ap,vendor,amount}] }] */
function aprRounds(data) {
  const apSet = new Set((data.payables || []).map(p => aprKey(p.vchno)).filter(Boolean));
  const paid = new Set((data.pvVouchers || []).map(p => aprKey(p.AP_No)).filter(Boolean));
  const since = (() => { const t = new Date(); t.setDate(t.getDate() - 14); return aprIsoOf(t.getFullYear(), t.getMonth() + 1, t.getDate()); })();
  const map = new Map();
  (data.forecastEntries || []).forEach(f => {
    if (String(f.EXPENSE_TYPE || '').toUpperCase() !== 'AP' || aprIsActual(f)) return;
    const iso = aprRoundOf(f);
    const ref = aprKey(f.REF_DOC);
    if (!iso || iso < since || !ref || paid.has(ref)) return;
    if (!map.has(iso)) map.set(iso, { iso, refs: new Set(), count: 0, sum: 0, nofound: [] });
    const o = map.get(iso);
    const amt = Math.abs(parseNum(f.AMOUNT));
    o.refs.add(ref); o.count++; o.sum += amt;
    if (!apSet.has(ref)) o.nofound.push({ ap: ref, amount: amt, desc: f.DESCRIPTION || '' });
  });
  return [...map.values()].sort((a, b) => (a.iso < b.iso ? -1 : 1));
}

function ApPayRoundStrip({ rounds, active, onFilter }) {
  const [openIso, setOpenIso] = React.useState('');
  if (!rounds.length) return null;
  return (
    <div style={{ display: 'grid', gap: 6, marginBottom: 14 }}>
      {rounds.map(r => {
        const on = active === r.iso;
        return (
          <div key={r.iso} className="card" style={{ padding: '8px 14px', borderLeft: '4px solid var(--brand-500)', borderRadius: 0 }}>
            <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 10, fontSize: 12.5 }}>
              <span style={{ fontWeight: 700, color: 'var(--ink-800)' }}>📅 รอบจ่ายบัญชี {aprFmt(r.iso)}</span>
              <span style={{ color: 'var(--ink-600)' }}>
                {r.count} ใบ · <strong style={{ color: 'var(--bad)', fontVariantNumeric: 'tabular-nums' }}>{fmtNum(r.sum, 2)}</strong>
              </span>
              {r.nofound.length > 0 && (
                <button onClick={() => setOpenIso(openIso === r.iso ? '' : r.iso)}
                  style={{ border: 'none', background: APR_WARN.bg, color: APR_WARN.color, borderRadius: 5, padding: '2px 8px', fontSize: 11.5, fontWeight: 700, cursor: 'pointer' }}>
                  ไม่มีในเจ้าหนี้คงค้าง {r.nofound.length} {openIso === r.iso ? '▴' : '▾'}
                </button>
              )}
              <span style={{ marginLeft: 'auto' }} />
              <button className="btn btn-ghost" style={{ height: 28, fontSize: 12, padding: '0 10px' }} onClick={() => onFilter(on ? '' : r.iso)}>
                {on ? '✕ แสดงทั้งหมด' : 'ดูเฉพาะรอบนี้'}
              </button>
            </div>
            {openIso === r.iso && r.nofound.length > 0 && (
              <div style={{ marginTop: 6, fontSize: 11.5, color: 'var(--ink-600)', lineHeight: 1.7 }}>
                <div style={{ color: 'var(--ink-400)' }}>วางแผนจ่ายไว้แล้ว (Bank Daily เห็นยอด) — พอนำเข้าเจ้าหนี้คงค้างที่มีเลขนี้ จะขึ้นในตารางเอง</div>
                {r.nofound.map(n => (
                  <div key={n.ap} style={{ display: 'flex', gap: 10 }}>
                    <span style={{ fontFamily: 'ui-monospace', color: 'var(--brand-700)', width: 120 }}>{n.ap}</span>
                    <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{n.desc}</span>
                    <span style={{ fontVariantNumeric: 'tabular-nums' }}>{fmtNum(n.amount, 2)}</span>
                  </div>
                ))}
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
}

Object.assign(window, { ApPayRoundModal, ApPayRoundStrip, aprRounds, aprRoundOf, aprParseWorkbook, aprClassify, APR_MARK });
