/* page_funding_plan.jsx — #funding_plan "แผนจัดหาเงินทุน"
 *
 *   ตอบ 3 คำถามของที่ประชุม: ต้องใช้เงินเท่าไร → ต้องเซ็นสัญญากี่บาทถึงจะเบิกเงินกู้ได้พอ → ต้องเซ็น/เบิกอะไร ภายในวันไหน
 *   เรียงตามงาน 7 ข้อของแผน: ค่าใช้จ่าย → เงินที่ขาด → Pipeline → เงื่อนไขสินเชื่อ → ยอดสัญญา → แผนเบิก → แผนสำรอง
 *
 *   เงินตั้งต้น / เงินเข้า / ค่าใช้จ่าย = ชุดเดียวกับการ์ด "แผนเงินสดล่วงหน้า" ใน Bank Daily (bd_avg_forecast.jsx)
 *     ใช้ฟังก์ชัน bdaf* + ยอดที่แก้/ล็อกไว้ (bd.afc.*) + เงินสำรองขั้นต่ำ (bd.afcfg) ตัวเดียวกัน → แก้ที่ Bank Daily แล้วหน้านี้ตาม
 *     ★ วิธีประมาณ + ฐานเดือน เก็บใน fund.cfg (ค่าเริ่ม auto · 6 เดือน) ไม่ตามค่าต่อคนของการ์ด — แผนนี้ทุกคนต้องเห็นเลขเดียวกัน
 *       auto = ไม่เอาเดือนที่จ่ายสูงผิดปกติมาคิด: ก.ย. 2569 มีมัดจำถังเหล็กก้อนเดียวจากเงินกู้ 152 ล้าน ค่าเฉลี่ยตรงๆ ของค่าโครงการสูงเกือบเท่าตัว
 *     ★ ตัดเฉพาะรหัสเงินต้น+ดอกเบี้ย (FUND_DEBT_GROUPS) ออกจากค่าเฉลี่ย เพราะใช้ตารางหนี้จริงแทน
 *       ส่วนที่เหลือของหมวดการเงิน (ค่าธรรมเนียม ค่าที่ปรึกษาแหล่งเงิน) ไม่มีในตารางหนี้ → นับเป็น Fixed Cost
 *     แถวที่ล็อก "ตกลงแล้ว" + ตารางหนี้ = จ่ายแน่นอน · ที่เหลือ = ประมาณการ
 *   เงินรับ = ใบแจ้งหนี้ที่มีวันคาดรับ + แผนรับ (กติกาเดียวกับการ์ด) + โครงการ "เซ็นแล้ว" ใน Pipeline ที่ใส่วันลูกค้าจ่าย
 *     ★ งวดที่ยังไม่ออกใบแจ้งหนี้ระบบไม่รู้ → ต้องใส่ใน Pipeline ไม่งั้นแผนมีแต่ค่าใช้จ่าย · หน้าเตือนเมื่อเงินรับที่นับได้ต่ำกว่าครึ่งของที่รับจริงเฉลี่ย
 *   ตารางหนี้ = debtMaster (Active) · ดอกเบี้ยจาก debtLedger (ลีซอิท = interestSchedulePrepaid) · ไม่มีแถว = คิดประมาณจากอัตราดอกเบี้ย
 *     ★ วันจ่ายดอกเบี้ย: WCI/Zigo รวมจ่ายสิ้นไตรมาส (Cash Flow จริง: 31 มี.ค. / 30 มิ.ย. / 30 ก.ย. ครั้งละ ~4.45 ล้าน) ที่เหลือสิ้นเดือน
 *     ★ ลีซอิท: paymentDate คือวันที่บนเช็ค เช็คลงวันล่วงหน้ายังไม่ออกจากบัญชี → นับตามวันเช็ค
 *     ★ เลขสัญญาซ้ำ (โอนมือแล้วตัวเก่ายังเปิดค้าง): แถวดอกเบี้ยที่ไม่มี contractId เป็นของสัญญาที่ครบหลังสุดเท่านั้น ไม่งั้นนับซ้ำ
 *     ★ ช่องวันครบของสัญญาเก่าบางแถวเก็บจำนวนเดือน ("6", "7") → ถือว่าไม่มีวันครบ (fundDue)
 *     ★ เลยกำหนดแล้วแต่ยัง Active = ค่าเริ่ม "รอตรวจ" ไม่นับเงินต้น: ของจริงส่วนใหญ่คือปิด/โอน/ต่อแล้วแต่ยังไม่อัปเดต · ขึ้นเตือนบนหน้า
 *     ★ สัญญานักลงทุนที่จะครบในช่วงแผน ค่าเริ่ม = ต่อสัญญา (cfg.debtDefault · ผู้ใช้เลือก 2026-11-03 · WCI ส่วนใหญ่ต่อมาแล้ว 4–6 รอบ)
 *       ถึง มิ.ย. 70 มีครบ 177.5 ล้าน ถ้านับว่าคืนหมด ยอดสัญญาที่ต้องเซ็นพุ่งเกิน 800 ล้าน · สัญญาที่จะคืนจริงเปลี่ยนรายสัญญา
 *       WCI ที่เลยกำหนดแล้วก็นับว่าต่อ (ผู้ใช้: "WCI ทุกคนเป็นต่อสัญญา") ยกเว้นเลยมาเกิน 1 ปี / ชื่อบอกว่าปิดหรือย้าย / แถวเก่าของเลขซ้ำ = รอตรวจ
 *     ★ สินเชื่อโอนสิทธิ์ (debtGroupOf = assign): ยอดรับจากลูกค้าถูกหักภาระหนี้ไปแล้ว (ivNetExpected)
 *       → ค่าเริ่ม = "หักจากเงินรับ" ไม่นับเงินต้นซ้ำ · นักลงทุน/ธนาคาร ค่าเริ่ม = "คืนจริง" · เปลี่ยนรายสัญญาได้
 *     ★ สัญญาสกุลต่างประเทศ: ไม่รวมจนกว่าจะใส่อัตราแลกเปลี่ยน (ห้ามบวกข้ามสกุล)
 *   เงินกู้ใหม่:
 *     · เงินเบิกรวมต่อโครงการ = สูงสุดตามอัตรา Post — Pre 50% แล้วค่อยเพิ่มเป็น Post 90% = เบิกเพิ่มอีก 40% ไม่ใช่ 140%
 *     · วงเงินจัดสรรตามลำดับวันเซ็น · "อัตโนมัติ" = แหล่งที่ต้นทุนต่ำกว่า (ถ้ารู้ค่าคอม+ดอกเบี้ยครบ) ไม่งั้น LIT ก่อน
 *     · ค่าคอม/ดอกเบี้ยที่ยังไม่ยืนยัน = นับเป็น 0 แล้วขึ้นเตือนบนหน้า — ไม่เดาตัวเลข
 *     · ดอกเบี้ยคิดลงสิ้นเดือน · ลูกค้าจ่ายในช่วงแผน = รับเงิน (หลังหัก 1%) แล้วคืนเงินต้น · ยอดที่มีใบแจ้งหนี้ค้างอยู่แล้วไม่บวกซ้ำ (fundCustIn)
 *       ไม่ใช้สินเชื่อก็ยังนับเงินรับ · กลุ่มเซ็นแล้วนับก่อนจัดหาเงินทุน กลุ่มอื่นนับตามสถานการณ์
 *   เซ็นแล้วมีเงินต้องจ่ายตามมา (ผู้ใช้ย้ำ 2026-11-03): ค่าของ/ถัง ค่าติดตั้ง ค่าคอมขาย ค่าอื่น (FUND_COSTS · % + รอบเวลาจริง)
 *     + หลักประกันสัญญา 5% (ใช้ LG ตามวงเงินแต่ละธนาคาร ไม่พอ = วางเงินสด · วงเงินไม่มีในระบบ ต้องใส่) · ของที่มัดจำไว้แล้วหักค่าของได้
 *       ออก LG ใบใหม่ได้เฉพาะธนาคารที่ติ๊ก (lgIssue · ค่าเริ่ม KBANK) — ผู้ใช้ 2026-11-03: KTB เป็นใบเก่าที่ยังค้ำงานเดิมอยู่ ไม่ออกใบใหม่แล้ว
 *       วงเงินที่นับ = "ว่างตอนนี้" ที่ใส่เอง (lgFree · ยอด LG ที่ค้างในหน้าหลักประกันอาจไม่ตรงกับธนาคาร จึงไม่เอามาหัก)
 *       KBANK เป็นวงเงิน 1 ต่อ 1 (เงินฝากค้ำเท่ายอด LG) ว่าง 9,921,016 ณ 3 พ.ย. 69 → ใช้หมดแล้วต้องวางเงินสดเท่ายอดหลักประกัน
 *     ลูกค้าจ่าย = วันที่ใส่เอง → งวดนับจากวันเซ็น (cfg.rcv · ผู้ใช้แจ้ง 2026-11-03: ~120 วันได้งวด 1 40% · อีก 60 วันได้งวดสุดท้าย 60%)
 *       โครงการที่กำลังทำนับเฉพาะงวดที่ยังไม่ได้รับ · คืนเงินกู้ตามสัดส่วนเงินที่ลูกค้าจ่าย ดอกเบี้ยคิดบนยอดคงค้าง
 *     ค่าคอมขาย 6% ของมูลค่าสัญญา (ผู้ใช้แจ้ง 2026-11-03 · ข้อมูลจ่ายจริงเฉลี่ยได้ 4.5%) ส่วนจังหวะจ่ายยังใช้ที่วัดจากของจริง
 *   ค่าใช้จ่ายโครงการเดิม (projMode 'jobs'): AP ที่ตั้งแล้วยังไม่จ่าย + ต้นทุนที่เหลือของโครงการที่กำลังทำ แทนค่าเฉลี่ยรหัสงานติดตั้ง (FUND_JOB_GROUPS)
 *     — ค่าเฉลี่ยมาจากช่วงที่มีงานหลายโครงการ ถ้าบวกต้นทุนโครงการใหม่ทับจะนับซ้ำ · เงินที่ยังไม่ได้รับของโครงการนั้นนับเป็น "คาดรับ (ประมาณ)"
 *   ยอดสัญญาที่ต้องเซ็น = จำลองสัญญาที่เซ็นวันเป้าหมาย (fundUnit) แล้วหายอดที่ทำให้ทุกวันที่เงินขาดกลับมาพอ (fundNeedContract)
 *     ใช้ LIT เต็มวงเงินก่อนแล้วเสริมอีกแห่ง · วันที่ต้นทุน/หลักประกันออกก่อนเงินกู้เข้า เซ็นเพิ่มก็ไม่ช่วย = ต้องมีเงินระยะสั้นคั่น
 *
 *   เก็บใน manualOverrides (แชร์ทั้งทีม · ไม่มี migration · ไม่แตะตารางหนี้/ประมาณการ/โครงการ):
 *     fund.cfg = เงื่อนไขสินเชื่อ+ค่าตั้ง (costs %, costOk, stockPaid, lgPct, lgFree/lgIssue รายธนาคาร, rcv งวดรับเงิน, projMode, debtDefault, endMonth) · fund.p.<id> = Pipeline รายแถว · fund.debt = { contractId: pay|roll|netted } · fund.check = เช็กลิสต์
 *   ค่าตั้งต่อคน (ส่วนที่กางอยู่/สถานการณ์ที่เลือก) = localStorage 'wtp.fund'
 *   ★ ชื่อ global ขึ้นต้น fund / Fund / FUND_ — ทุกไฟล์ .jsx แชร์ scope เดียวกัน
 */
'use strict';

const FUND_LS        = 'wtp.fund';
const FUND_CFG_KEY   = 'fund.cfg';
const FUND_DEBT_KEY  = 'fund.debt';
const FUND_CHECK_KEY = 'fund.check';
const FUND_PIPE      = 'fund.p.';
/* ต้นทุนหลังเซ็นสัญญา — วัดจากโครงการที่เซ็น มิ.ย. 67 – มี.ค. 69 (PV จ่ายจริง + ใบรับเงิน)
 *   pct = % ของมูลค่าสัญญา (รวม VAT) · curve = % ของรายการนั้นที่จ่ายในแต่ละช่วงวันหลังเซ็น (FUND_CURVE_DAYS)
 *   ค่าของ = PV คลังกลาง ม.ค. 68 – ส.ค. 69 ÷ สัญญาที่เซ็น ก.ย. 67 – เม.ย. 69 (โยงรายโครงการได้แค่ 31% จึงใช้ภาพรวม) */
const FUND_CURVE_DAYS = [[0, 30], [31, 60], [61, 90], [91, 120], [121, 150], [151, 210], [211, 300]];
const FUND_COSTS = [
  { k: 'goods',   t: 'ค่าของและถัง (แอดวานซ์)',   pct: 39,  curve: [1.7, 3.5, 8.3, 10.1, 16.9, 47.7, 11.8] },
  { k: 'install', t: 'ค่าติดตั้ง',                 pct: 14,  curve: [4.3, 3.8, 6.5, 15.4, 7.7, 23.8, 38.5] },
  { k: 'comm',    t: 'ค่าคอมขาย',                  pct: 6,   curve: [29, 14.5, 7, 3, 17.5, 15.9, 13.1] },   // ผู้ใช้แจ้ง 6% (ของจริงเฉลี่ย 4.5%)
  { k: 'other',   t: 'ค่าใช้จ่ายอื่นของโครงการ',   pct: 6,   curve: [22.8, 22.7, 18.7, 13, 11.5, 5.1, 6.2] },
];
const FUND_RCV_DEFAULT = { d1: 120, p1: 40, gap: 60 };        // ผู้ใช้แจ้ง: เซ็นแล้ว ~120 วันได้งวด 1 (40%) · อีก 60 วันได้งวดสุดท้าย (60%)
const FUND_JOB_GROUPS = ['20001', '20002', '20003', '20004', '20005', '20010'];   // ต้นทุนงานติดตั้งใน Cash Flow — โหมดงานจริงคิดจาก AP + ต้นทุนที่เหลือแทนค่าเฉลี่ย
const FUND_DEBT_GROUPS = ['60100', '60200', '60400', '60500'];   // เงินต้น+ดอกเบี้ยใน Cash Flow — มีในตารางหนี้แล้ว
const FUND_QUARTER_CATS = ['WCI', 'Zigo'];                       // ดอกเบี้ยรวมจ่ายสิ้นไตรมาส

const FUND_GROUPS = [
  { k: 'signed',   t: 'เซ็นแล้ว',       c: '#15803d', bg: '#f0fdf4' },
  { k: 'soon',     t: 'กำลังจะเซ็น',    c: '#1d4ed8', bg: '#eff6ff' },
  { k: 'approval', t: 'รออนุมัติ',      c: '#b45309', bg: '#fffbeb' },
  { k: 'unsure',   t: 'ยังไม่แน่นอน',   c: '#64748b', bg: '#f1f5f9' },
];
const FUND_GROUP_OF = FUND_GROUPS.reduce((m, g) => { m[g.k] = g; return m; }, {});
const FUND_DEBT_TREAT = {
  pay:    'คืนจริง (เงินออก)',
  roll:   'ต่อสัญญา (ไม่ออกเงินต้น)',
  netted: 'หักจากเงินรับลูกค้า',
  review: 'เลยกำหนด · รอตรวจ (ยังไม่นับ)',
};
const FUND_DEFAULT_CFG = {
  endMonth: '2027-06', targetDate: '2026-12-10', lastDate: '2026-12-25',
  delayDays: 14, fx: { USD: null, EUR: null }, expMode: 'auto', lookback: 6, projMode: 'jobs', debtDefault: 'roll',
  costs: {}, costOk: false, stockPaid: null, lgPct: 5, rcv: FUND_RCV_DEFAULT,
  lgIssue: { KBANK: true }, lgFree: { KBANK: 9921016 }, lgAsOf: '2026-11-03',     // ผู้ใช้แจ้ง 3 พ.ย. 69 · แก้ในหน้าได้
  lenders: {
    lit: { name: 'LIT',              pre: 50, post: 90,   fee: null, rate: null, limit: 43000000, leadDays: 14, ok: { pre: true, post: true } },
    alt: { name: 'สินเชื่ออีกแห่ง', pre: 60, post: null, fee: null, rate: 6,    limit: null,     leadDays: 14, ok: { rate: true } },
  },
};
const FUND_LENDER_FIELDS = [
  { f: 'pre',      t: 'อัตราเบิก Pre',  suffix: '%' },
  { f: 'post',     t: 'อัตราเบิก Post', suffix: '%' },
  { f: 'fee',      t: 'ค่าคอมมิชชัน',   suffix: '% ของยอดเบิก' },
  { f: 'rate',     t: 'อัตราดอกเบี้ย',  suffix: '% ต่อปี' },
  { f: 'limit',    t: 'วงเงินคงเหลือ',  suffix: 'บาท' },
  { f: 'leadDays', t: 'เซ็นจนเงินเข้า', suffix: 'วัน' },
];
const FUND_CHECKLIST = [
  ['จัดทำ Fixed Cost และภาระหนี้เดือน พ.ย.–ม.ค.', 'วันนี้ / เร่งด่วน'],
  ['สรุปเงินสดต้นงวดและรายรับที่มีความแน่นอน', 'วันนี้ / เร่งด่วน'],
  ['คำนวณ Funding Gap รายเดือนและยอดสะสม', 'หลังรวบรวมงบประมาณ'],
  ['ตรวจสอบเงื่อนไขสินเชื่อทั้งสองแหล่ง', 'คู่ขนานกับการจัดทำ Cash Flow'],
  ['นำข้อมูลโครงการจากฝ่ายขายมาจัดทำ Pipeline', 'เร่งขอข้อมูล'],
  ['คำนวณยอดสัญญาที่ต้องเซ็นเพิ่ม', 'เมื่อทราบ Funding Gap และเงื่อนไขสินเชื่อ'],
  ['จัดทำแผนเบิกเงินกู้และวันเงินเข้า', 'หลังมีข้อมูลวันเซ็นสัญญา'],
  ['ทำ Scenario และแผนสำรอง', 'ก่อนนำเสนอผู้บริหาร'],
  ['จัดทำ Dashboard และสรุปข้อเสนอผู้บริหาร', 'งานสุดท้าย'],
];

/* ── ตัวช่วย ───────────────────────────────────────────────────────── */
function fundLoadPrefs() { try { return JSON.parse(localStorage.getItem(FUND_LS) || '{}') || {}; } catch (_) { return {}; } }
function fundSavePrefs(p) { try { localStorage.setItem(FUND_LS, JSON.stringify(p)); } catch (_) {} }
function fundJson(v) {
  if (v && typeof v === 'object') return v;
  try { const o = JSON.parse(v); return o && typeof o === 'object' ? o : null; } catch (_) { return null; }
}
function fundP(v) { return (v == null || v === '' || !isFinite(Number(v))) ? null : Number(v) / 100; }
function fundN(v) { return (v == null || v === '' || !isFinite(Number(v))) ? null : Number(v); }
function fundAddDays(iso, n) { const d = new Date(iso + 'T00:00:00'); d.setDate(d.getDate() + n); return bdISO(d); }
function fundDays(a, b) { return Math.round((new Date(b + 'T00:00:00') - new Date(a + 'T00:00:00')) / 86400000); }
function fundMonthEnd(mk) { return mk + '-' + String(bdafDim(mk)).padStart(2, '0'); }
function fundM(n) { return (n < 0 ? '−' : '') + fmtNum(Math.abs(n || 0) / 1e6, 2) + ' ล้าน'; }
function fundB(n) { return (n < 0 ? '−' : '') + fmtNum(Math.round(Math.abs(n || 0)), 0); }
function fundCode(v) { return String(v || '').trim().toUpperCase(); }
function fundDue(v) { const s = String(v == null ? '' : v).trim(); return /^\d{1,3}$/.test(s) ? '' : bdToISO(s); }   // สัญญาเก่าเก็บจำนวนเดือนไว้ในช่องวันครบ
function fundIsDebtLine(l) { return FUND_DEBT_GROUPS.indexOf(String(l.code || '')) >= 0; }
/* ดอกเบี้ยของเดือนนี้จ่ายวันไหน */
function fundIntDay(mk, quarterly) {
  if (!quarterly) return fundMonthEnd(mk);
  return fundMonthEnd(mk.slice(0, 5) + String(Math.ceil(+mk.slice(5, 7) / 3) * 3).padStart(2, '0'));
}

function fundReadOv(rows) {
  const out = { cfgRaw: null, debt: {}, check: {}, pipe: [] };
  (rows || []).forEach(r => {
    if (!r || typeof r.key !== 'string' || r.key.indexOf('fund.') !== 0) return;
    const o = fundJson(r.value);
    if (!o) return;
    if (r.key === FUND_CFG_KEY) out.cfgRaw = o;
    else if (r.key === FUND_DEBT_KEY) out.debt = o;
    else if (r.key === FUND_CHECK_KEY) out.check = o;
    else if (r.key.indexOf(FUND_PIPE) === 0) out.pipe.push(Object.assign({}, o, { id: r.key.slice(FUND_PIPE.length) }));
  });
  out.pipe.sort((a, b) => String(a.signDate || '9999').localeCompare(String(b.signDate || '9999')) || String(a.name || '').localeCompare(String(b.name || ''), 'th'));
  return out;
}
function fundCfgOf(raw) {
  const r = raw || {}, d = FUND_DEFAULT_CFG, lenders = {};
  Object.keys(d.lenders).forEach(k => {
    const a = d.lenders[k], b = (r.lenders && r.lenders[k]) || {};
    lenders[k] = Object.assign({}, a, b, { ok: Object.assign({}, a.ok, b.ok) });
  });
  return Object.assign({}, d, r, { lenders, fx: Object.assign({}, d.fx, r.fx), costs: Object.assign({}, r.costs), lgIssue: Object.assign({}, d.lgIssue, r.lgIssue), lgFree: Object.assign({}, d.lgFree, r.lgFree), rcv: Object.assign({}, d.rcv, r.rcv) });
}
function fundLender(cfg, k) {
  const L = cfg.lenders[k];
  return { k, name: L.name, pre: fundP(L.pre), post: fundP(L.post), fee: fundP(L.fee), rate: fundP(L.rate), limit: fundN(L.limit), lead: Number(L.leadDays) || 0 };
}
/* ช่องที่ยังไม่ยืนยัน → เตือนบนหน้า · ช่องว่าง = ยังไม่รู้ */
function fundUnconfirmed(cfg) {
  const out = [];
  Object.keys(cfg.lenders).forEach(k => {
    const L = cfg.lenders[k];
    FUND_LENDER_FIELDS.forEach(x => {
      if (x.f === 'leadDays' || (L.ok && L.ok[x.f])) return;
      out.push(L.name + ': ' + x.t + (L[x.f] == null || L[x.f] === '' ? ' (ยังไม่รู้)' : ''));
    });
  });
  return out;
}

/* ── แผนรับเงิน: กรองแผนที่ปิดแล้วแบบเดียวกับ Bank Daily ─────────────────── */
function fundForecasts(data, arAll) {
  const forecasts = (data.forecastEntries || []).map(bdNormForecast);
  const closed = new Set(), known = new Set(), paidAp = new Set();
  arAll.forEach(a => {
    const no = String(a.ivNo || '').trim();
    if (!no) return;
    known.add(no);
    if (a.status === 'paid' || !(a.amount > 0.01)) closed.add(no);
  });
  (data.pvVouchers || []).map(bdNormPV).forEach(p => { if (p.apNo) paidAp.add(String(p.apNo).trim()); });
  return forecasts.map(f => Object.assign({}, f, { settled: bdForecastSettled(f, { paidAp, closedIv: closed, knownIv: known }) }));
}

/* ── ตารางหนี้จริงในช่วงแผน ────────────────────────────────────────────── */
function fundDebtRows(data, F, today, cfg, choice) {
  const first = F[0], lastDay = fundMonthEnd(F[F.length - 1]);
  const ledger = data.debtLedger || [], prepaid = data.interestSchedulePrepaid || [];
  const out = [], fxMissing = {}, byId = {}, byNo = {}, noOwner = {};
  ledger.forEach(r => {
    const map = r.contractId ? byId : byNo, k = r.contractId || r.contractNo;
    (map[k] = map[k] || []).push(r);
  });
  const isLitOf = (m) => m.leasitSource === true || m.leasitSource === 'true';
  const dueOf = (m) => fundDue(isLitOf(m) ? (m.leasitDateDueRoll || m.leasitDateDue || m.maturityDate) : (m.maturityDate || m.endDate));
  const active = (data.debtMaster || []).filter(m => m && String(m.status || '').trim() === 'Active');
  active.forEach(m => {
    const cur = noOwner[m.contractNo];
    if (!cur || (dueOf(m) || '9999') > (dueOf(cur) || '9999')) noOwner[m.contractNo] = m;   // เลขซ้ำ: ไม่มีวันครบ = ยังเดินอยู่ ถือเป็นตัวล่าสุด
  });
  // ดอกเบี้ยรายไตรมาสของเดือนก่อนหน้าในไตรมาสเดียวกัน ยังจ่ายในช่วงแผน → ต้องไล่ตั้งแต่ต้นไตรมาส
  const qMonths = bdafRange(first.slice(0, 5) + String(Math.floor((+first.slice(5, 7) - 1) / 3) * 3 + 1).padStart(2, '0'), F[F.length - 1]);
  active.forEach(m => {
    const cur = debtCur(m);
    const fx = cur === 'THB' ? 1 : (Number(cfg.fx[cur]) || 0);
    const bal = debtDisplayBalance(m);
    const isLit = isLitOf(m);
    const due = dueOf(m);
    const inHz = !!due && due <= lastDay;
    const overdue = !!due && due < today;
    // WCI ทุกคนต่อสัญญา (ผู้ใช้ 2026-11-03) — เลยกำหนดแล้วก็นับว่าต่อ เว้นแถวที่น่าจะปิด/โอนไปแล้ว: เลยมาเกิน 1 ปี · ชื่อบอกว่าปิดหรือย้าย · แถวเก่าของเลขซ้ำ
    const staleRow = fundDays(due || today, today) > 365 || /ปิด|ย้าย/.test(String(m.contractNo || '') + ' ' + String(m.borrowerName || '')) || noOwner[m.contractNo] !== m;
    const defTreat = debtGroupOf(m) === 'assign' ? 'netted'
      : overdue ? (m.debtCategory === 'WCI' && cfg.debtDefault !== 'pay' && !staleRow ? 'roll' : 'review')
      : cfg.debtDefault === 'pay' ? 'pay' : 'roll';
    const treat = (inHz && FUND_DEBT_TREAT[choice[m.id]]) ? choice[m.id] : defTreat;
    const intM = {};
    let est = false;
    const addInt = (mk, date, amt, isEst) => {
      if (!(amt > 0.005)) return;
      intM[mk] = intM[mk] || { amt: 0, evs: [] };
      intM[mk].amt += amt;
      intM[mk].evs.push({ date: date < today ? today : date, amt });
      if (isEst) est = true;
    };
    if (isLit) {
      // ดอกลีซอิทจ่ายล่วงหน้าด้วยเช็ครายงวด · paymentDate = วันที่บนเช็ค → เช็คลงวันล่วงหน้ายังไม่ออกจากบัญชี
      prepaid.filter(r => LIT_sameLoan(r.loanId, m.leasitLoanId) && !r.lenderCancelled).forEach(r => {
        const d = bdToISO(r.paymentDate) || bdToISO(r.dateStart);
        if (d && d >= today && d <= lastDay) addInt(d.slice(0, 7), d, Number(r.intAmount) || 0, false);
      });
    } else if (treat !== 'netted') {
      const quarterly = FUND_QUARTER_CATS.indexOf(m.debtCategory) >= 0;
      const inPlan = (mk) => { if (!quarterly) return F.indexOf(mk) >= 0; const d = fundIntDay(mk, true); return d >= today && d <= lastDay; };
      const rows = (byId[m.id] || []).concat(noOwner[m.contractNo] === m ? (byNo[m.contractNo] || []) : []);
      const seen = {};
      rows.forEach(r => {
        const mk = r.year + '-' + String(r.month).padStart(2, '0');
        if (!inPlan(mk)) return;
        seen[mk] = true;
        addInt(mk, quarterly ? fundIntDay(mk, true) : (bdToISO(r.periodEnd) || fundMonthEnd(mk)), interestRemaining(r), false);
      });
      // ไม่มีแถวดอกเบี้ยของเดือนนั้น (สัญญาที่ยังไม่ได้เปิดตารางล่วงหน้า) → ประมาณจากยอดคงเหลือ × อัตรา
      const rate = Number(m.interestRate) || 0, start = bdToISO(debtStartOf(m));
      if (rate > 0 && bal > 0) (quarterly ? qMonths : F).forEach(mk => {
        if (seen[mk] || !inPlan(mk) || (due && treat !== 'roll' && mk > due.slice(0, 7))) return;
        const from0 = (!quarterly && mk === first) ? today : mk + '-01';
        const from = start > from0 ? start : from0;
        const stop = (due && treat !== 'roll' && due < fundMonthEnd(mk)) ? due : bdafAddM(mk, 1) + '-01';
        const days = fundDays(from, stop);
        if (days > 0) addInt(mk, fundIntDay(mk, quarterly), bal * rate * days / 365, true);
      });
    }
    const pri = (inHz && treat === 'pay' && bal > 0) ? bal : 0;
    const hasInt = Object.keys(intM).length > 0;
    if (!inHz && !hasInt) return;
    if (!fx && (pri > 0 || hasInt)) fxMissing[cur] = (fxMissing[cur] || 0) + 1;
    const evs = [];
    if (fx) {
      if (pri > 0) evs.push({ date: due < today ? today : due, amt: -pri * fx, kind: 'principal', certain: true });
      Object.keys(intM).forEach(mk => intM[mk].evs.forEach(e => evs.push({ date: e.date, amt: -e.amt * fx, kind: 'interest', certain: !est })));
    }
    out.push({
      m, id: m.id, no: m.contractNo || '', name: m.borrowerName || '', cat: m.debtCategory || '', cur, fx, bal, due, overdue,
      inHz, treat, defTreat, pri: pri * (fx || 0), int: Object.keys(intM).reduce((s, k) => s + intM[k].amt, 0) * (fx || 0), est, evs,
    });
  });
  // รอตรวจขึ้นก่อน → ครบกำหนดที่ต้องคืน → หักจากเงินรับ → ยังไม่ครบ (มีแต่ดอกเบี้ย)
  const rank = (d) => (d.treat === 'review' ? 0 : !d.inHz ? 3 : d.treat === 'netted' ? 2 : 1);
  out.sort((a, b) => rank(a) - rank(b) || String(a.due || '9999').localeCompare(String(b.due || '9999')));
  return { rows: out, fxMissing };
}

/* ── เงินรับจากลูกค้า ───────────────────────────────────────────────── */
function fundArOpen(arAll) {
  const o = {};
  arAll.forEach(a => {
    const k = fundCode(a.jobNo);
    if (k && a.status !== 'paid') o[k] = (o[k] || 0) + (Number(a.raw && a.raw.balance) || 0) * 106 / 107;
  });
  return o;
}
/* งวดรับเงินนับจากวันเซ็น: [{ day, share }] (cfg.rcv · ค่าเริ่ม วันที่ 120 ได้ 40% · อีก 60 วันได้ 60%) */
function fundRcvPlan(cfg) {
  const r = Object.assign({}, FUND_RCV_DEFAULT, cfg && cfg.rcv);
  const d1 = Math.max(0, Number(r.d1) || 0), p1 = Math.min(100, Math.max(0, Number(r.p1) || 0)), gap = Math.max(0, Number(r.gap) || 0);
  return [{ day: d1, share: p1 / 100 }, { day: d1 + gap, share: 1 - p1 / 100 }].filter(x => x.share > 0);
}
function fundPayDays(cfg) { return Math.round(fundRcvPlan(cfg).reduce((s, x) => s + x.day * x.share, 0)); }   // วันรับเงินถัวเฉลี่ย ใช้เทียบต้นทุนเงินกู้
function fundBuildDays(cfg) { const pl = fundRcvPlan(cfg); return pl.length ? pl[0].day : FUND_RCV_DEFAULT.d1; }   // ส่งมอบ ≈ งวดแรก (วันเบิก Post)
/* วันลูกค้าจ่าย: ใส่เองชนะ → งวดนับจากวันเซ็นจริง · ถึงวันแล้วยังไม่ได้รับ = อีก 30 วัน */
function fundRcvDates(r, today, cfg) {
  const late = (d) => (d < today ? fundAddDays(today, 30) : d);
  if (r.payDate) return [{ date: r.payDate < today ? today : r.payDate, share: 1, est: false }];
  if (!r.signDate) return [];
  return fundRcvPlan(cfg).map(x => ({ date: late(fundAddDays(r.signDate, x.day)), share: x.share, est: true }));
}
/* เงินเข้าหลังหัก 1% · ยอดที่มีใบแจ้งหนี้ค้างอยู่แล้วนับในเงินรับแล้ว ไม่บวกซ้ำ */
function fundCustIn(r, today, lastDay, arOpen, cfg) {
  const amt = (Number(r.value) || 0) * 106 / 107 - ((arOpen && arOpen[fundCode(r.code)]) || 0);
  if (!(amt > 0.005)) return [];
  return fundRcvDates(r, today, cfg).filter(x => x.date <= lastDay).map(x => ({ date: x.date, amt: amt * x.share, est: x.est }));
}
/* รับจริงจากลูกค้าเฉลี่ยต่อเดือน (รหัสเงินรับขึ้นต้น 1) — ไว้เทียบว่าแผนนับเงินรับได้แค่ไหน */
function fundCustAvg(txns, months) {
  if (!months || !months.length) return 0;
  const set = new Set(months);
  return (txns || []).reduce((s, t) => {
    const v = Number(t.flow) || 0;
    return v > 0 && String(t.code || '').charAt(0) === '1' && set.has(String(t.iso || '').slice(0, 7)) ? s + v : s;
  }, 0) / months.length;
}

/* ── ต้นทุนหลังเซ็น · งานเดิม · หลักประกัน ─────────────────────────────── */
function fundMid(i) { const w = FUND_CURVE_DAYS[i]; return Math.round((w[0] + w[1]) / 2); }
function fundCostsOf(cfg) { return FUND_COSTS.map(c => Object.assign({}, c, { pct: fundN(cfg.costs[c.k]) != null ? Number(cfg.costs[c.k]) : c.pct })); }
function fundCostPct(cfg) { return fundCostsOf(cfg).reduce((s, c) => s + c.pct, 0); }
function fundIsJobLine(l) { return FUND_JOB_GROUPS.indexOf(String(l.code || '')) >= 0; }
/* จ่ายครบครึ่งของรายการภายในวันที่เท่าไรหลังเซ็น */
function fundHalfDay(curve) {
  let s = 0;
  for (let i = 0; i < curve.length; i++) { s += curve[i]; if (s >= 50) return FUND_CURVE_DAYS[i][1]; }
  return FUND_CURVE_DAYS[curve.length - 1][1];
}
/* จ่ายไปแล้วต่อโครงการจาก PV: โครงการตรง + ของจากคลังกลางที่หมายเหตุระบุรหัสโครงการ (แบ่งเท่ากันถ้าระบุหลายโครงการ) */
function fundPaidByCode(pv) {
  const o = {};
  (pv || []).forEach(r => {
    const a = Number(r.Net_Amount) || 0;
    if (!a) return;
    const pd = String(r.Project_Dpt || ''), m = pd.match(/\)\s*([A-Za-z]{2,6}\d{2,4})-/);
    if (m) { const k = m[1].toUpperCase(); o[k] = (o[k] || 0) + a; return; }
    if (!/Warehouse/i.test(pd)) return;
    const cs = Array.from(new Set((String(r.Remark || '') + ' ' + String(r.cc_remark || '')).toUpperCase().match(/\b[A-Z]{2,6}\d{2,4}\b/g) || []));
    cs.forEach(c => { o[c] = (o[c] || 0) + a / cs.length; });
  });
  return o;
}
/* งานเดิม: AP ที่ตั้งแล้วยังไม่จ่าย (ไม่รวม AP แผนก — อยู่ใน Fixed Cost) + ต้นทุนที่เหลือของโครงการที่กำลังทำ
 *   + เงินที่ยังไม่ได้รับของโครงการนั้น (ยังไม่ออกใบแจ้งหนี้ · หักภาระหนี้โอนสิทธิ · มีแถวใน Pipeline = ใช้แถวนั้นแทน) */
function fundJobs(data, projs, fcs, arOpen, finance, cfg, today, lastDay, pipe) {
  const codeOf = (t) => (typeof prCodeFromText === 'function' ? prCodeFromText(t) : '');
  const paidAp = new Set((data.pvVouchers || []).map(p => String(p.AP_No || '').trim()).filter(Boolean));
  const plan = {};
  fcs.forEach(f => { if (f.amount < 0 && !f.isActual && !f.settled && f.date && String(f.expType).toUpperCase() === 'AP' && f.refDoc) plan[String(f.refDoc).trim()] = f.date; });
  const evs = [], apBy = {}, ap = { n: 0, amt: 0, late: 0 }, wip = [], late30 = fundAddDays(today, -30);
  // AP แผนกอยู่ใน Fixed Cost แล้ว · ใบลดหนี้นับทุกใบแม้ลงแผนก (ของจริง: CNS2023 ลงแผนก EN ลดหนี้ใบงานเก่า
  //   ส่วน APS ที่ตั้งใหม่อยู่ใต้โครงการ) แล้วหักกับเจ้าหนี้รายเดียวกัน ไม่เกินยอดที่ค้างรายนั้น
  const list = [], pos = {}, neg = {};
  (data.payables || []).forEach(p => {
    const a = bdNormAP(p), no = String(a.vchno || '').trim();
    if (!(Math.abs(a.amount) > 0.5) || paidAp.has(no) || (a.amount > 0 && p.dpt_code)) return;
    list.push({ p, a, no });
    if (a.amount > 0) pos[a.vendor] = (pos[a.vendor] || 0) + a.amount; else neg[a.vendor] = (neg[a.vendor] || 0) - a.amount;
  });
  list.forEach(({ p, a, no }) => {
    const amt = a.amount < 0 && neg[a.vendor] > (pos[a.vendor] || 0) ? a.amount * (pos[a.vendor] || 0) / neg[a.vendor] : a.amount;
    if (!(Math.abs(amt) > 0.5)) return;
    const c = codeOf(p.pre_des), d0 = plan[no] || a.due || today, date = d0 < today ? today : d0;
    if (c) apBy[c] = (apBy[c] || 0) + amt;
    ap.n++; ap.amt += amt;
    if (!plan[no] && a.due && a.due < late30) ap.late += amt;
    if (date <= lastDay) evs.push({ date, amt: -amt, kind: 'proj', sub: 'ap', certain: true });
  });
  const paid = fundPaidByCode(data.pvVouchers);
  const costs = fundCostsOf(cfg), cp = costs.reduce((s, c) => s + c.pct, 0);
  const mix = FUND_CURVE_DAYS.map((_, i) => costs.reduce((s, c) => s + c.pct * c.curve[i], 0) / (cp || 1));
  const inPipe = new Set(pipe.filter(r => !r.off).map(r => fundCode(r.code)).filter(Boolean));
  projs.filter(p => p.status === 'Work in progress').forEach(p => {
    const c = fundCode(p.contractNo), v = Number(p.contractAmt) || 0, s = bdToISO(p.start), fin = bdToISO(p.finish);
    if (!c || !(v > 0)) return;
    const spent = (paid[c] || 0) + (apBy[c] || 0);
    const left = (fin && fin < today) ? 0 : Math.max(0, v * cp / 100 - spent);      // เลยกำหนดเสร็จแล้ว = เหลือแค่ AP ที่ตั้งไว้
    const x = { code: c, name: p.site || p.name, v, spent, left, leftPlan: 0, rcv: 0, rcvPlan: 0 };
    if (left > 0.5) {
      const fut = s ? mix.map((w, i) => ({ w, date: fundAddDays(s, fundMid(i)) })).filter(o => o.w > 0 && o.date >= today) : [];
      const W = fut.reduce((t, o) => t + o.w, 0);
      (W ? fut : [{ w: 1, date: fundAddDays(today, 30) }]).forEach(o => {
        const a = left * o.w / (W || 1);
        if (o.date <= lastDay) { evs.push({ date: o.date, amt: -a, kind: 'proj', sub: 'wip' }); x.leftPlan += a; }
      });
    }
    if (!inPipe.has(c)) {
      const debt = Number((finance[c] || finance[p.contractNo] || {}).debt) || 0;
      x.rcv = Math.max(0, (Number(p.outstandingAR) || 0) * 106 / 107 - ((arOpen && arOpen[c]) || 0) - debt);
      if (x.rcv > 0.5 && s) {
        // รับไปแล้วกี่ % ของสัญญา → เหลือเฉพาะงวดที่ยังไม่ได้รับ (เช่น ได้งวด 1 แล้ว = เหลืองวดสุดท้ายอย่างเดียว)
        const got = Math.min(1, (Number(p.received) || 0) / v);
        let acc = 0;
        const rest = fundRcvPlan(cfg).map(o => { const lo = acc; acc += o.share; return { day: o.day, w: Math.max(0, acc - Math.max(lo, got)) }; }).filter(o => o.w > 1e-6);
        const W = rest.reduce((t, o) => t + o.w, 0);
        (W ? rest : [{ day: fundRcvPlan(cfg).slice(-1)[0].day, w: 1 }]).forEach(o => {
          const d0 = fundAddDays(s, o.day), date = d0 < today ? fundAddDays(today, 30) : d0, a = x.rcv * o.w / (W || 1);
          if (date <= lastDay) { evs.push({ date, amt: a, kind: 'wipIn' }); x.rcvPlan += a; }
        });
      }
    }
    wip.push(x);
  });
  return { evs, ap, wip };
}
/* หลักประกันที่ยังไม่ได้คืน: LG แยกธนาคาร + เงินสด · ครบในช่วงแผน · เลยกำหนดแล้วยังไม่ได้คืน */
function fundLgBook(rows, today, lastDay) {
  const norm = (b) => (typeof gtNormBank === 'function' ? gtNormBank(b) : String(b || '').trim().toUpperCase()) || 'ไม่ระบุ';
  const out = { banks: {}, cash: 0, lgSoon: 0, cashSoon: 0, lgLate: 0, cashLate: 0 };
  (rows || []).forEach(r => {
    if (r.receivedDate || /คืนแล้ว|ยกเลิก/.test(String(r.status || ''))) return;
    const a = Number(r.amount) || 0;
    if (!(a > 0)) return;
    const cash = /เงินสด|cash/i.test(String(r.guaranteeType || '')), end = bdToISO(r.endDate);
    if (cash) out.cash += a; else { const b = norm(r.bank); out.banks[b] = (out.banks[b] || 0) + a; }
    if (end && end < today) { if (cash) out.cashLate += a; else out.lgLate += a; }
    else if (end && end <= lastDay) { if (cash) out.cashSoon += a; else out.lgSoon += a; }
  });
  return out;
}
/* วงเงิน LG ที่ยังว่าง: เฉพาะธนาคารที่ออกใบใหม่ (lgIssue) ตามยอด "ว่างตอนนี้" ที่ใส่ไว้ (lgFree)
 *   ไม่มีธนาคารที่ออกใบใหม่ = 0 (วางเงินสดทั้งหมด) · มีแต่ยังไม่ใส่ยอด = ยังไม่รู้ (null) */
function fundLgIssuers(cfg) { return Object.keys(cfg.lgIssue || {}).filter(b => cfg.lgIssue[b]); }
function fundLgRoom(book, cfg) {
  const banks = fundLgIssuers(cfg);
  if (!banks.length) return 0;
  const ks = banks.filter(b => fundN(cfg.lgFree[b]) != null);
  if (!ks.length) return null;
  return ks.reduce((s, b) => s + Math.max(0, Number(cfg.lgFree[b])), 0);
}

/* ── เงินเบิกของโครงการ: Pre แล้วเพิ่มถึง Post — รวมกันไม่เกินอัตรา Post ───────── */
function fundAnchor(date, today) { return date ? (date > today ? date : today) : ''; }   // เซ็น/ส่งมอบไปแล้ว = เริ่มนับจากวันนี้ เพราะยังไม่ได้ยื่นเรื่อง
function fundTranches(r, L, delay, today) {
  const v = Number(r.value) || 0, d = r.dates || {}, wait = delay || 0;
  const sign = fundAnchor(r.signDate, today), deliver = fundAnchor(r.deliverDate || r.deliverEst, today);
  const cash = d.cash ? fundAddDays(d.cash, wait) : (sign ? fundAddDays(sign, L.lead + wait) : '');
  const after = deliver ? fundAddDays(deliver, L.lead + wait) : '';
  const stage = r.stage === 'post' ? 'post' : 'pre';
  const pre = L.pre || 0, post = L.post || 0, out = [];
  if (stage === 'pre' && pre > 0) out.push({ at: 'pre', date: cash, gross: v * pre });
  const top = stage === 'post' ? post : Math.max(0, post - pre);
  if (top > 0) out.push({ at: 'post', date: after, gross: v * top });
  return out;
}
/* ต้นทุนต่อเงินเบิก 1 บาท · รู้ไม่ครบ = null (ไม่เอาตัวเลขที่ไม่รู้มาตัดสิน) */
function fundCostRate(L, days) {
  if (L.fee == null || L.rate == null) return null;
  return L.fee + L.rate * days / 365;
}
function fundPick(r, lit, alt, room, lastDay, today, cfg) {
  const need = (L) => fundTranches(r, L, 0, today).filter(t => t.date && t.date <= lastDay).reduce((s, t) => s + t.gross, 0);   // นับเฉพาะที่เบิกในช่วงแผน
  const fits = (L) => need(L) > 0 && room(L) >= need(L);
  if (!need(lit) && !need(alt)) return lit;
  const a = fits(lit), b = fits(alt);
  if (a && b) {
    const days = (r.payDate && r.signDate) ? Math.max(0, fundDays(r.signDate, r.payDate)) : fundPayDays(cfg);
    const ca = fundCostRate(lit, days), cb = fundCostRate(alt, days);
    return (ca != null && cb != null && cb < ca) ? alt : lit;
  }
  if (a) return lit;
  if (b) return alt;
  return (need(lit) > 0 && room(lit) >= room(alt)) ? lit : alt;
}
/* ดอกเบี้ยเงินกู้ใหม่: คิดบนยอดคงค้างรายวัน ลงสิ้นเดือน · flows = +เบิก / −คืน */
function fundLoanCost(flows, rate, lastDay) {
  const pts = flows.filter(f => f.date <= lastDay).sort((a, b) => a.date.localeCompare(b.date));
  if (!(rate > 0) || !pts.length) return [];
  const out = [], end = fundAddDays(lastDay, 1);
  let bal = 0, acc = 0, i = 0, at = pts[0].date;
  while (at < end) {
    while (i < pts.length && pts[i].date <= at) { bal += pts[i].amt; i++; }
    if (bal <= 0.005 && i >= pts.length) break;
    const mk = at.slice(0, 7), me = bdafAddM(mk, 1) + '-01';
    let nx = me < end ? me : end;
    if (i < pts.length && pts[i].date < nx) nx = pts[i].date;
    acc += Math.max(0, bal) * rate * fundDays(at, nx) / 365;
    if ((nx === me || nx === end) && acc > 0.005) { out.push({ date: nx === me ? fundMonthEnd(mk) : lastDay, amt: -acc, kind: 'fundCost' }); acc = 0; }
    at = nx;
  }
  if (acc > 0.005) out.push({ date: at > lastDay ? lastDay : at, amt: -acc, kind: 'fundCost' });
  return out;
}
/* เหตุการณ์เงินของโครงการ 1 แถว: ต้นทุนหลังเซ็น · หลักประกัน · เงินรับ · เบิก/คืน/ดอกเบี้ยเงินกู้
 *   ctx (แชร์ระหว่างแถว ตามลำดับวันเซ็น): used/room = วงเงินกู้ · lgRoom = วงเงิน LG ว่าง (null = ยังไม่รู้) · pool = ของที่จ่ายล่วงหน้าแล้ว
 *   เซ็นแล้ว = ต้นทุน/เงินรับนับในงานเดิมหรือก่อนจัดหาเงินทุนแล้ว เหลือแค่เงินกู้ */
function fundRowEvents(r, L, cfg, scen, today, lastDay, ctx) {
  const v = Number(r.value) || 0, evs = [], isNew = r.group !== 'signed';
  const sign = fundAnchor(r.signDate, today);
  const x = { evs, tr: [], gross: 0, net: 0, fee: 0, cut: false, cost: 0, costPlan: 0, lgNeed: 0, lgCash: 0, lgUnknown: false, rcvPlan: 0 };
  if (isNew && sign) {
    fundCostsOf(cfg).forEach(c => {
      let amt = v * c.pct / 100;
      if (c.k === 'goods' && ctx.pool > 0) { const use = Math.min(ctx.pool, amt); ctx.pool -= use; amt -= use; }
      x.cost += amt;
      c.curve.forEach((s, i) => {
        const a = amt * s / 100, d = fundAddDays(sign, fundMid(i));
        if (a > 0.005 && d <= lastDay) { evs.push({ date: d, amt: -a, kind: 'newCost', sub: c.k }); x.costPlan += a; }
      });
    });
    const need = v * (Number(cfg.lgPct) || 0) / 100;
    if (need > 0) {
      x.lgNeed = need;
      if (ctx.lgShare != null) x.lgCash = v * ctx.lgShare;
      else if (ctx.lgRoom == null) x.lgUnknown = true;
      else { const use = Math.min(ctx.lgRoom, need); ctx.lgRoom -= use; x.lgCash = need - use; }
      if (x.lgCash > 0.005 && sign <= lastDay) evs.push({ date: sign, amt: -x.lgCash, kind: 'lgCash' });
    }
  }
  if (isNew) fundCustIn(r, today, lastDay, ctx.arOpen, cfg).forEach(e => { evs.push({ date: e.date, amt: e.amt, kind: 'custIn' }); x.rcvPlan += e.amt; });
  if (!L) return x;
  const fee = L.fee || 0, flows = [];
  x.tr = fundTranches(r, L, scen.delay, today).map(t => {
    const date = t.date ? (t.date < today ? today : t.date) : '';
    const inHz = !!date && date <= lastDay;
    if (!inHz) return Object.assign({}, t, { date, net: t.gross * (1 - fee), inHz });     // ยังไม่มีวัน/เบิกหลังช่วงแผน = ไม่กินวงเงินในแผนนี้
    let g = t.gross;
    const room = ctx.room(L);
    if (g > room) { g = room; x.cut = true; }
    ctx.used[L.k] += g;
    x.gross += g; x.net += g * (1 - fee); x.fee += g * fee;
    if (g > 0.005) { evs.push({ date, amt: g * (1 - fee), kind: 'fund' }); flows.push({ date, amt: g }); }
    return Object.assign({}, t, { date, gross: g, net: g * (1 - fee), inHz });
  });
  // คืนเงินกู้ตามสัดส่วนเงินที่ลูกค้าจ่าย แล้วคิดดอกเบี้ยบนยอดคงค้าง
  if (x.gross > 0.005) {
    let out = 0;
    flows.map(f => ({ date: f.date, draw: f.amt }))
      .concat(fundRcvDates(r, today, cfg).filter(o => o.date <= lastDay).map(o => ({ date: o.date, share: o.share })))
      .sort((a, b) => a.date.localeCompare(b.date) || (a.draw ? -1 : 1))
      .forEach(p => {
        if (p.draw) { out += p.draw; return; }
        const pay = Math.min(out, x.gross * p.share);
        if (pay > 0.005) { out -= pay; evs.push({ date: p.date, amt: -pay, kind: 'fundRepay' }); flows.push({ date: p.date, amt: -pay }); }
      });
    fundLoanCost(flows, L.rate || 0, lastDay).forEach(e => evs.push(e));
  }
  return x;
}
/* จัดสรรวงเงินกู้และวงเงิน LG ตามลำดับวันเซ็น → รายการเบิก + เหตุการณ์เงินเข้า-ออก */
function fundAllocate(rows, cfg, scen, today, lastDay, ctx0) {
  const lit = fundLender(cfg, 'lit'), alt = fundLender(cfg, 'alt');
  const used = { lit: 0, alt: 0 };
  const room = (L) => (L.limit == null ? Infinity : Math.max(0, L.limit - used[L.k]));
  const ctx = { arOpen: ctx0.arOpen, lgRoom: ctx0.lgRoom, pool: Number(cfg.stockPaid) || 0, used, room };
  const keep = rows.filter(r => !r.off && Number(r.value) > 0 && scen.groups.indexOf(r.group) >= 0)
    .sort((a, b) => String(a.signDate || '9999').localeCompare(String(b.signDate || '9999')));
  const items = [], evs = [];
  keep.forEach(r => {
    const noLoan = r.assignable === 'no' || r.lender === 'none';
    const L = noLoan ? null : scen.forceAlt ? alt : r.lender === 'lit' ? lit : r.lender === 'alt' ? alt : fundPick(r, lit, alt, room, lastDay, today, cfg);
    const x = fundRowEvents(r, L, cfg, scen, today, lastDay, ctx);
    x.evs.forEach(e => evs.push(e));
    const first = x.tr.find(t => t.date);
    items.push(Object.assign(x, { r, L, cashDate: first ? first.date : '',
      why: noLoan ? (r.assignable === 'no' ? 'โอนสิทธิไม่ได้' : 'ไม่ใช้สินเชื่อ') : (first ? '' : 'ยังไม่มีวันเซ็น') }));
  });
  return { items, evs, used, net: items.reduce((s, x) => s + x.net, 0), lgLeft: ctx.lgRoom, poolLeft: ctx.pool };
}

/* ── เดินเงินสดรายวัน: วัดยอดสิ้นวัน ─────────────────────────────────────
 *   ต่างจาก bdafCash (วันเดียวกันลงเงินออกก่อน) เพราะต้นทุน/เงินรับตามรอบเวลาจริงลงวันกลางช่วงวันเดียวกัน
 *   ถ้าลงเงินออกก่อนจะเกิดจุดต่ำสุดปลอม (สัญญา 100 ล้าน: หนุน 22 ล้าน แทน 13 ล้าน) */
function fundWalk(F, today, opening, buffer, evs) {
  const by = {};
  F.forEach(mk => { by[mk] = []; });
  evs.forEach(e => { const mk = e.date.slice(0, 7); if (by[mk]) by[mk].push(e); });
  let bal = opening, cum = 0;
  return F.map((mk, i) => {
    const ev = by[mk].sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));
    const open = bal, start = i === 0 ? today : mk + '-01', sum = {};
    let run = bal, min = bal, minDate = start;
    let short = (open + cum < buffer) ? start : null;
    ev.forEach((e, j) => {
      run += e.amt;
      sum[e.kind] = (sum[e.kind] || 0) + e.amt;
      if (ev[j + 1] && ev[j + 1].date === e.date) return;
      if (run < min) { min = run; minDate = e.date; }
      if (!short && run + cum < buffer) short = e.date;
    });
    const cumNew = Math.max(cum, buffer - min), need = cumNew - cum;
    cum = cumNew; bal = run;
    return { mk, open, sum, close: run, min, minDate, need, needCum: cum, short: need > 0.5 ? short : null };
  });
}
function fundScenarios(cfg) {
  const all = ['signed', 'soon', 'approval'];
  return [
    { k: 'base',  t: 'ตามแผน',            en: 'Base',        hint: 'เซ็นได้ตามแผน เงินกู้เข้าตามกำหนด (ไม่นับ "ยังไม่แน่นอน")', groups: all, delay: 0 },
    { k: 'down',  t: 'เซ็นได้บางโครงการ', en: 'Downside',    hint: 'นับเฉพาะ "เซ็นแล้ว" กับ "กำลังจะเซ็น"', groups: ['signed', 'soon'], delay: 0 },
    { k: 'delay', t: 'เบิกล่าช้า',         en: 'Delay',       hint: 'เซ็นได้ครบ แต่เงินเข้าช้า ' + (Number(cfg.delayDays) || 0) + ' วัน', groups: all, delay: Number(cfg.delayDays) || 0 },
    { k: 'alt',   t: 'เปลี่ยนแหล่งเงิน',   en: 'Alternative', hint: 'ใช้ ' + cfg.lenders.alt.name + ' ทุกโครงการ', groups: all, delay: 0, forceAlt: true },
  ];
}

/* ── ยอดสัญญาที่ต้องเซ็น ─────────────────────────────────────────────── */
const FUND_UNIT = 1000000;
/* เงินสดของสัญญาที่เซ็นวัน sign ต่อมูลค่า 1 บาท (L = null คือไม่กู้) · lgShare = สัดส่วนหลักประกันที่ต้องวางเป็นเงินสด */
function fundUnit(cfg, L, sign, today, lastDay, lgShare) {
  const ctx = { arOpen: null, lgRoom: null, lgShare: lgShare || 0, pool: 0, used: { lit: 0, alt: 0 }, room: () => Infinity };
  const r = { value: FUND_UNIT, signDate: sign, deliverEst: fundAddDays(sign, fundBuildDays(cfg)), group: 'soon', stage: 'pre' };
  const x = fundRowEvents(r, L, cfg, { delay: 0 }, today, lastDay, ctx);
  return { gross: x.gross / FUND_UNIT, evs: x.evs.map(e => ({ date: e.date, amt: e.amt / FUND_UNIT, kind: e.kind, sub: e.sub })).sort((a, b) => a.date.localeCompare(b.date)) };
}
function fundCum(evs, d) { let s = 0; for (let i = 0; i < evs.length && evs[i].date <= d; i++) s += evs[i].amt; return s; }
/* วันที่เงินต่ำกว่าเงินสำรอง และขาดเท่าไร (ยอดสิ้นวัน) */
function fundShorts(evs, opening, buffer, today) {
  const by = {}, out = [];
  evs.forEach(e => { by[e.date] = (by[e.date] || 0) + e.amt; });
  let bal = opening;
  if (bal < buffer - 0.5) out.push({ date: today, s: buffer - bal });
  Object.keys(by).sort().forEach(d => { bal += by[d]; if (bal < buffer - 0.5) out.push({ date: d < today ? today : d, s: buffer - bal }); });
  return out;
}
/* ยอดสัญญาที่ทำให้ทุกวันที่ขาดกลับมาพอ = มากสุดของ (ขาด ÷ เงินสุทธิสะสมต่อสัญญา 1 บาท ณ วันนั้น)
 *   ใช้แหล่งแรกเต็มวงเงินก่อน (room1 = ยอดเบิกที่ยังเบิกได้ · null = ไม่จำกัด) แล้วเสริมด้วยแหล่งที่สอง
 *   วันที่สัญญายังไม่ให้เงินสุทธิ (ต้นทุน/หลักประกันออกก่อนเงินกู้เข้า) เซ็นเพิ่มก็ไม่ช่วย = ต้องมีเงินระยะสั้นคั่น */
function fundNeedContract(shorts, u1, room1, u2) {
  const res = { c1: 0, c2: 0, total: 0, bridge: 0, bridgeTo: '' };
  if (!shorts.length || !u1) return res;
  const cap1 = room1 != null && u1.gross > 0 ? room1 / u1.gross : Infinity;
  let c1 = 0;
  shorts.forEach(p => { const u = fundCum(u1.evs, p.date); if (u > 1e-6) c1 = Math.max(c1, p.s / u); });
  if (c1 > cap1 && u2) {
    let c2 = 0;
    shorts.forEach(p => { const left = p.s - cap1 * fundCum(u1.evs, p.date), u = fundCum(u2.evs, p.date); if (left > 0.5 && u > 1e-6) c2 = Math.max(c2, left / u); });
    res.c1 = cap1; res.c2 = c2;
  } else res.c1 = c1;
  res.total = res.c1 + res.c2;
  shorts.forEach(p => {
    const gap = p.s - res.c1 * fundCum(u1.evs, p.date) - (u2 ? res.c2 * fundCum(u2.evs, p.date) : 0);
    if (gap > 0.5) { if (gap > res.bridge) res.bridge = gap; res.bridgeTo = p.date; }
  });
  return res;
}
/* จุดต่ำสุดของเงินสะสมต่อสัญญา (ยอดสิ้นวัน · ค่าติดลบ = เงินที่ต้องหนุน) */
function fundTrough(u, sign) {
  let c = 0, min = 0, at = sign;
  u.evs.forEach((e, j) => { c += e.amt; if (u.evs[j + 1] && u.evs[j + 1].date === e.date) return; if (c < min) { min = c; at = e.date; } });
  return { min, day: fundDays(sign, at) };
}

/* ── เทียบ 2 แหล่งรายโครงการ: ค่าคอมสินเชื่อ + ดอกเบี้ยจนลูกค้าจ่าย ───────────── */
function fundCompare(r, L, cfg, today) {
  const ctx = { arOpen: null, lgRoom: null, lgShare: 0, pool: 0, used: { lit: 0, alt: 0 }, room: () => Infinity };
  const x = fundRowEvents(Object.assign({}, r, { group: 'signed' }), L, cfg, { delay: 0 }, today, fundAddDays(today, 1100), ctx);
  const interest = -x.evs.filter(e => e.kind === 'fundCost').reduce((s, e) => s + e.amt, 0);
  const first = x.tr.find(t => t.date);
  return { gross: x.gross, net: x.net, feeAmt: x.fee, interest: L.rate == null ? null : interest,
    total: L.fee != null && L.rate != null ? x.fee + interest : null,
    days: first && r.signDate ? fundDays(fundAnchor(r.signDate, today), first.date) : null };
}
/* แผนเดินเอกสาร: วันที่กรอกเองชนะ ไม่งั้นแบ่งตามจำนวนวันจากเซ็นถึงเงินเข้าของแหล่งนั้น */
function fundMilestones(r, L, today) {
  const d = r.dates || {}, s = fundAnchor(r.signDate, today), n = L ? L.lead : 0;
  const at = (k, pct) => d[k] || (s ? fundAddDays(s, Math.round(n * pct)) : '');
  return { submit: at('submit', 0.2), assign: at('assign', 0.5), approve: at('approve', 0.8), cash: at('cash', 1) };
}

/* ── ส่งออก Excel ─────────────────────────────────────────────────────── */
function fundExportXlsx(x) {
  const X = window.XLSX;
  if (!X) return false;
  const wb = X.utils.book_new();
  const add = (name, aoa, widths) => { const ws = X.utils.aoa_to_sheet(aoa); ws['!cols'] = widths.map(w => ({ wch: w })); X.utils.book_append_sheet(wb, ws, name); };
  const R = (v) => Math.round(v || 0);
  const { F, walk0, res, scens, need0, cfg, debt, alloc, pipe, totals } = x;
  add('สรุปผู้บริหาร', [
    ['แผนจัดหาเงินทุน', bdafMShort(F[0]) + ' – ' + bdafMShort(F[F.length - 1])],
    ['ข้อมูล ณ วันที่', x.today],
    [],
    ['ต้องใช้เงินรวม', R(totals.out)],
    ['เงินทุนที่ยังขาด (ก่อนจัดหาเงิน)', R(totals.need)],
    ['ยอดสัญญาที่ต้องเซ็น (รวมต้นทุนหลังเซ็นแล้ว)', R(need0.total)],
    ['เงินระยะสั้นที่ต้องคั่นก่อนเงินกู้เข้า', R(need0.bridge)],
    ['วงเงินกู้สุทธิที่คาดว่าจะได้ (' + res.scen.t + ')', R(res.alloc.net)],
    ['ยังขาดหลังเบิก (' + res.scen.t + ')', R(res.need)],
    [],
    ['ช่องที่ยังไม่ยืนยัน'].concat(x.unconfirmed),
  ], [38, 18]);
  const months = F.map(bdafMShort);
  const line = (t, fn) => [t].concat(F.map((_, i) => R(fn(i))));
  add('เงินสดรายเดือน', [
    ['รายการ'].concat(months),
    line('เงินสดต้นเดือน', i => walk0[i].open),
    line('รับชำระจากลูกค้า', i => walk0[i].sum.ar),
    line('คาดรับจากโครงการที่กำลังทำ (ประมาณ)', i => walk0[i].sum.wipIn),
    line('รายรับอื่นที่แน่นอน', i => walk0[i].sum.other),
    line('Fixed Cost', i => walk0[i].sum.fixed),
    line('ชำระเงินต้น', i => walk0[i].sum.principal),
    line('ดอกเบี้ย', i => walk0[i].sum.interest),
    line('ค่าใช้จ่ายโครงการเดิม', i => walk0[i].sum.proj),
    line('เงินสดก่อนจัดหาเงินทุน', i => walk0[i].close),
    line('จุดต่ำสุดในเดือน', i => walk0[i].min),
    line('Funding Gap รายเดือน', i => walk0[i].need),
    line('Funding Gap สะสม', i => walk0[i].needCum),
    line('เงินกู้สุทธิเข้า (' + res.scen.t + ')', i => res.walk[i].sum.fund),
    line('เงินรับจากโครงการใหม่ (' + res.scen.t + ')', i => res.walk[i].sum.custIn),
    line('ต้นทุนโครงการใหม่ (' + res.scen.t + ')', i => res.walk[i].sum.newCost),
    line('หลักประกันที่วางเป็นเงินสด (' + res.scen.t + ')', i => res.walk[i].sum.lgCash),
    line('ดอกเบี้ยและคืนเงินกู้ (' + res.scen.t + ')', i => (res.walk[i].sum.fundCost || 0) + (res.walk[i].sum.fundRepay || 0)),
    line('เงินสดหลังจัดหาเงินทุน', i => res.walk[i].close),
  ], [30].concat(F.map(() => 15)));
  add('ภาระหนี้', [['เลขสัญญา', 'เจ้าหนี้', 'หมวด', 'ครบกำหนด', 'สกุล', 'คงเหลือ', 'เงินต้นที่นับ (บาท)', 'ดอกเบี้ยในช่วงแผน (บาท)', 'นับอย่างไร']]
    .concat(debt.rows.map(d => [d.no, d.name, d.cat, d.due, d.cur, R(d.bal), R(d.pri), R(d.int), FUND_DEBT_TREAT[d.treat]])), [16, 30, 12, 12, 6, 15, 16, 18, 22]);
  add('Pipeline', [['โครงการ', 'รหัส', 'มูลค่าสัญญา', 'กลุ่ม', 'วันคาดเซ็น', 'วันส่งมอบ/ตรวจรับ', 'วันลูกค้าจ่าย', 'โอนสิทธิได้', 'แหล่งเงิน', 'เบิกสุทธิรวม', 'เงินเข้าวันแรก', 'ต้นทุนทั้งโครงการ', 'ต้นทุนในช่วงแผน', 'หลักประกัน', 'วางเป็นเงินสด', 'หมายเหตุ']]
    .concat(pipe.map(r => {
      const a = alloc.items.find(i => i.r.id === r.id);
      return [r.name, r.code, R(r.value), (FUND_GROUP_OF[r.group] || {}).t || '', r.signDate, r.deliverDate, r.payDate, r.assignable === 'yes' ? 'ได้' : r.assignable === 'no' ? 'ไม่ได้' : 'ยังไม่รู้',
        a && a.L ? a.L.name : '', a ? R(a.net) : '', a ? a.cashDate || '' : '', a ? R(a.cost) : '', a ? R(a.costPlan) : '', a ? R(a.lgNeed) : '', a ? (a.lgUnknown ? 'ยังไม่รู้วงเงิน LG' : R(a.lgCash)) : '', r.note || ''];
    })), [34, 12, 15, 14, 12, 14, 12, 10, 14, 15, 13, 15, 15, 13, 15, 26]);
  add('แผนสำรอง', [['สถานการณ์', 'เงินกู้สุทธิเข้า', 'จุดต่ำสุด', 'วันต่ำสุด', 'เริ่มขาด', 'ต้องหาทดแทน']]
    .concat(scens.map(s => [s.scen.t + ' (' + s.scen.en + ')', R(s.alloc.net), R(s.lowest.min), s.lowest.minDate, s.firstShort ? s.firstShort.short : '', R(s.need)])), [28, 16, 15, 12, 12, 15]);
  X.writeFile(wb, 'แผนจัดหาเงินทุน_' + x.today + '.xlsx');
  return true;
}

/* ── ชิ้นส่วนหน้าจอ ───────────────────────────────────────────────────── */
function FundNum({ value, onChange, disabled, placeholder, width }) {
  const [txt, setTxt] = React.useState(null);
  const shown = txt != null ? txt : (value == null || value === '' ? '' : fmtNum(Number(value), Number(value) % 1 ? 2 : 0));
  return (
    <input className="input" inputMode="decimal" disabled={disabled} placeholder={placeholder} value={shown} style={{ width: width || 120, textAlign: 'right' }}
      onFocus={() => setTxt(value == null ? '' : String(value))}
      onChange={e => setTxt(e.target.value)}
      onBlur={e => {
        const t = String(e.target.value).replace(/[,\s฿%]/g, '');
        setTxt(null);
        const n = t === '' ? null : Number(t);
        if (n === null || isFinite(n)) { if (n !== fundN(value)) onChange(n); }
      }} />
  );
}
function FundKpi({ label, value, sub, tone }) {
  const c = tone === 'bad' ? 'var(--bad)' : tone === 'good' ? 'var(--good)' : 'var(--ink-900)';
  return (
    <div className="card" style={{ padding: '14px 16px', minWidth: 0 }}>
      <div style={{ fontSize: 12, color: 'var(--ink-500)', fontWeight: 600 }}>{label}</div>
      <div style={{ fontSize: 24, fontWeight: 700, color: c, marginTop: 4, fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' }}>{value}</div>
      {sub && <div style={{ fontSize: 12, color: 'var(--ink-500)', marginTop: 4, lineHeight: 1.45 }}>{sub}</div>}
    </div>
  );
}
function FundStep({ no, title, sum, open, onToggle, right, children }) {
  return (
    <div className="card" style={{ padding: 0, marginBottom: 12, overflow: 'hidden' }}>
      <div onClick={onToggle} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '11px 16px', cursor: 'pointer', borderBottom: open ? '1px solid var(--line-soft)' : 'none' }}>
        <span style={{ fontSize: 10, color: 'var(--ink-400)', transform: open ? 'rotate(90deg)' : 'none', transition: 'transform .15s' }}>▶</span>
        {no != null && <span style={{ width: 22, height: 22, flex: '0 0 22px', borderRadius: 11, background: 'var(--brand-50)', color: 'var(--brand-700)', fontSize: 12, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>{no}</span>}
        <div style={{ minWidth: 0, flex: 1 }}>
          <div style={{ fontWeight: 700, fontSize: 14, color: 'var(--ink-800)' }}>{title}</div>
          {sum && <div style={{ fontSize: 12, color: 'var(--ink-500)', marginTop: 2 }}>{sum}</div>}
        </div>
        {right && <div onClick={e => e.stopPropagation()}>{right}</div>}
      </div>
      {open && children}
    </div>
  );
}
function FundChip({ t, tone }) {
  const s = tone === 'bad' ? ['var(--bad-bg)', 'var(--bad)'] : tone === 'warn' ? ['var(--warn-bg)', '#92400e'] : tone === 'good' ? ['var(--good-bg)', '#15803d'] : ['#f1f5f9', '#475569'];
  return <span style={{ fontSize: 11, fontWeight: 600, padding: '1px 7px', borderRadius: 4, background: s[0], color: s[1], whiteSpace: 'nowrap', marginRight: 4 }}>{t}</span>;
}

/* ฟอร์มแถว Pipeline */
function FundRowModal({ row, cfg, canEdit, onSave, onDelete, onClose }) {
  const [d, setD] = React.useState(row);
  const set = (k, v) => setD(p => Object.assign({}, p, { [k]: v }));
  const lab = { fontSize: 12, fontWeight: 600, color: 'var(--ink-600)' };
  const field = (label, node, hint) => (
    <label style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
      <span style={lab}>{label}</span>{node}
      {hint && <span style={{ fontSize: 11, color: 'var(--ink-500)' }}>{hint}</span>}
    </label>
  );
  const date = (k) => <input className="input" type="date" disabled={!canEdit} value={d[k] || ''} onChange={e => set(k, e.target.value)} />;
  const ok = String(d.name || '').trim() && Number(d.value) > 0;
  return (
    <Modal open onClose={onClose} title={row.id ? 'แก้ไขโครงการใน Pipeline' : 'เพิ่มโครงการใน Pipeline'} maxWidth={720}
      footer={<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
        <div>{row.id && canEdit && <button className="btn btn-danger" onClick={() => onDelete(row)}>ลบแถวนี้</button>}</div>
        <div style={{ display: 'flex', gap: 8 }}>
          <button className="btn" onClick={onClose}>ยกเลิก</button>
          {canEdit && <button className="btn btn-primary" disabled={!ok} onClick={() => onSave(d)}>บันทึก</button>}
        </div>
      </div>}>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(210px, 1fr))', gap: 12 }}>
        {field('ชื่อโครงการ *', <input className="input" disabled={!canEdit} value={d.name || ''} onChange={e => set('name', e.target.value)} />)}
        {field('รหัสโครงการ', <input className="input" disabled={!canEdit} value={d.code || ''} onChange={e => set('code', e.target.value)} />)}
        {field('มูลค่าสัญญา (บาท) *', <FundNum width="100%" disabled={!canEdit} value={d.value} onChange={v => set('value', v)} />)}
        {field('กลุ่ม', <select className="select input" disabled={!canEdit} value={d.group || 'soon'} onChange={e => set('group', e.target.value)}>
          {FUND_GROUPS.map(g => <option key={g.k} value={g.k}>{g.t}</option>)}
        </select>)}
        {field('วันคาดว่าจะเซ็น', date('signDate'))}
        {field('วันส่งมอบ/ตรวจรับ', date('deliverDate'), 'ใช้ประเมินวันเบิกแบบ Post')}
        {field('วันลูกค้าคาดว่าจะจ่าย', date('payDate'), 'ว่าง = แบ่งงวดตามข้อ 5 นับจากวันเซ็น (ค่าเริ่ม 120 วัน 40% · อีก 60 วัน 60%) · ใบแจ้งหนี้ที่ค้างอยู่ไม่บวกซ้ำ')}
        {field('โอนสิทธิลูกหนี้ได้ไหม', <select className="select input" disabled={!canEdit} value={d.assignable || ''} onChange={e => set('assignable', e.target.value)}>
          <option value="">ยังไม่รู้</option><option value="yes">ได้</option><option value="no">ไม่ได้</option>
        </select>)}
        {field('แหล่งเงิน', <select className="select input" disabled={!canEdit} value={d.lender || 'auto'} onChange={e => set('lender', e.target.value)}>
          <option value="auto">ให้ระบบเลือก</option>
          <option value="lit">{cfg.lenders.lit.name}</option>
          <option value="alt">{cfg.lenders.alt.name}</option>
          <option value="none">ไม่ใช้สินเชื่อ</option>
        </select>)}
        {field('เริ่มเบิกแบบ', <select className="select input" disabled={!canEdit} value={d.stage || 'pre'} onChange={e => set('stage', e.target.value)}>
          <option value="pre">Pre (เซ็นแล้วเบิกได้)</option><option value="post">Post (หลังส่งมอบ)</option>
        </select>)}
        {field('หมายเหตุ', <input className="input" disabled={!canEdit} value={d.note || ''} onChange={e => set('note', e.target.value)} />)}
      </div>
    </Modal>
  );
}

/* เลือกโครงการจากหน้าโครงการ: รอลงนาม + เซ็นแล้วที่ยังเก็บเงินไม่ครบ (มีผู้รับโอนสิทธิแล้ว = เพิ่มเป็นเงินค้างรับ ไม่ใช่สัญญาใหม่) */
function FundPickModal({ data, taken, onPick, onClose }) {
  const [q, setQ] = React.useState('');
  const list = React.useMemo(() => {
    if (!window.PCU) return [];
    return window.PCU.deriveProjects(data.projects || [], data.invoices || [], data.receipts || [], data.debtMaster || [])
      .filter(p => p.status === 'ยังไม่ลงนาม' || (p.status === 'Work in progress' && p.outstandingAR > 0))
      .sort((a, b) => (a.status === b.status ? (b.contractAmt || 0) - (a.contractAmt || 0) : a.status === 'ยังไม่ลงนาม' ? 1 : -1));
  }, []);
  const hits = list.filter(p => !q.trim() || (p.name + ' ' + p.contractNo + ' ' + p.province).toLowerCase().indexOf(q.trim().toLowerCase()) >= 0).slice(0, 120);
  return (
    <Modal open onClose={onClose} title="เลือกจากโครงการในระบบ" maxWidth={820}>
      <div style={{ fontSize: 12.5, color: 'var(--ink-500)', marginBottom: 8 }}>โครงการที่เซ็นแล้วและยังเก็บเงินไม่ครบ กับโครงการรอลงนาม · {list.length} โครงการ</div>
      <input className="input" autoFocus placeholder="ค้นหาชื่อ / เลขสัญญา / จังหวัด" value={q} onChange={e => setQ(e.target.value)} style={{ width: '100%', marginBottom: 10 }} />
      <div style={{ maxHeight: 420, overflow: 'auto' }}>
        {hits.map(p => {
          const isTaken = taken.has(p.id);
          return (
            <div key={p.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '7px 2px', borderBottom: '1px solid var(--line-soft)' }}>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 13, color: 'var(--ink-800)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.name}</div>
                <div style={{ fontSize: 11.5, color: 'var(--ink-500)' }}>{p.status === 'ยังไม่ลงนาม' ? 'รอลงนาม · ' + (p.projectStatus || '') : (p.assignee ? 'เซ็นแล้ว · มีผู้รับโอนสิทธิแล้ว' : 'เซ็นแล้ว · ยังไม่มีผู้รับโอนสิทธิ') + ' · ค้างรับ ' + fundB(p.outstandingAR)} · {p.province}</div>
              </div>
              <div style={{ fontVariantNumeric: 'tabular-nums', fontSize: 13, whiteSpace: 'nowrap' }}>{fundB(p.contractAmt || p.allocation || 0)}</div>
              <button className="btn btn-sm" disabled={isTaken} onClick={() => onPick(p)}>{isTaken ? 'เพิ่มแล้ว' : '＋ เพิ่ม'}</button>
            </div>
          );
        })}
        {!hits.length && <div style={{ padding: 20, textAlign: 'center', color: 'var(--ink-500)', fontSize: 13 }}>ไม่พบโครงการ</div>}
      </div>
    </Modal>
  );
}

/* ── หน้าหลัก ─────────────────────────────────────────────────────────── */
function FundingPlanPage({ data, toast }) {
  useOverrideSubAny();
  const canEdit = window.WTPAuth ? window.WTPAuth.can('canEdit') : true;
  const today = bdISO(new Date());
  const cm = today.slice(0, 7);
  const pref0 = React.useMemo(fundLoadPrefs, []);
  const prefB = React.useMemo(bdafLoadPrefs, []);
  const [open, setOpen] = React.useState(pref0.open || { 2: true });
  const [scenK, setScenK] = React.useState(pref0.scen || 'base');
  const [editRow, setEditRow] = React.useState(null);
  const [picking, setPicking] = React.useState(false);
  const [detail, setDetail] = React.useState({});
  React.useEffect(() => { fundSavePrefs({ open, scen: scenK }); }, [open, scenK]);
  const tog = (k) => setOpen(p => Object.assign({}, p, { [k]: !p[k] }));

  const { cf, cfState } = bdafUseCf();
  const ovRows = data.manualOverrides || [];
  const ovSig = ovRows.filter(r => r && typeof r.key === 'string' && (r.key.indexOf('fund.') === 0 || r.key.indexOf(BDAF_PREFIX) === 0 || r.key === BDAF_CFG_KEY)).map(r => r.key + '=' + r.value).join('|');
  const ov  = React.useMemo(() => fundReadOv(ovRows), [ovSig]);
  const cfg = React.useMemo(() => fundCfgOf(ov.cfgRaw), [ov]);
  const buffer = Number(bdafReadCfg(ovRows).buffer) || 0;
  const hz = cfg.endMonth >= cm ? cfg.endMonth : bdafAddM(cm, 2);

  /* ค่าใช้จ่าย + เงินเข้า = ชุดเดียวกับการ์ดใน Bank Daily · วิธีคิด/ฐานเดือนเก็บใน fund.cfg ให้ทุกคนเห็นเลขเดียวกัน */
  const mode = cfg.expMode === 'avg' ? 'avg' : 'auto';
  const lookback = BDAF_LOOKBACKS.indexOf(Number(cfg.lookback)) >= 0 ? Number(cfg.lookback) : 6;
  const jobsMode = cfg.projMode !== 'avg';
  const ed   = React.useMemo(() => bdafParseEdits(ovRows), [ovSig]);
  const col  = React.useMemo(() => bdafCollectCf(cf && cf.stm && cf.stm.txns, cf && cf.codeGroups, today), [cf, today]);
  const plan = React.useMemo(() => bdafBuild(col, lookback, hz, ed.edits), [col, lookback, hz, ed]);
  const view = React.useMemo(() => bdafView(plan, mode), [plan, mode]);
  const F = plan.F, lastDay = fundMonthEnd(F[F.length - 1]);
  const accounts = React.useMemo(() => (data.bankAccounts || []).map(bdAcct), [data.bankAccounts]);
  const opening = React.useMemo(() => bdafOpening(accounts), [accounts]);
  const openAmt = prefB.openBase === 'main' && opening.mainN ? opening.main : opening.all;
  const finance = React.useMemo(() => (WTPData.buildLookups ? (WTPData.buildLookups(data).financeByCode || {}) : {}), [data.projects, data.debtLedger, data.debtMaster]);
  const arAll   = React.useMemo(() => (data.invoices || []).map(iv => bdNormAR(iv, finance)), [data.invoices, finance]);
  const fcs     = React.useMemo(() => fundForecasts(data, arAll), [data.forecastEntries, data.pvVouchers, arAll]);
  const rcv     = React.useMemo(() => bdafReceipts(arAll.filter(a => a.status !== 'paid'), fcs, today, F), [arAll, fcs, today, F]);
  const debt    = React.useMemo(() => fundDebtRows(data, F, today, cfg, ov.debt), [data.debtMaster, data.debtLedger, data.interestSchedulePrepaid, F, today, cfg, ov.debt]);
  const arOpen  = React.useMemo(() => fundArOpen(arAll), [arAll]);
  const custAvg = React.useMemo(() => fundCustAvg(cf && cf.stm && cf.stm.txns, plan.H), [cf, plan]);
  const projs   = React.useMemo(() => (window.PCU ? window.PCU.deriveProjects(data.projects || [], data.invoices || [], data.receipts || [], data.debtMaster || []) : []), [data.projects, data.invoices, data.receipts, data.debtMaster]);
  const jobs    = React.useMemo(() => fundJobs(data, projs, fcs, arOpen, finance, cfg, today, lastDay, ov.pipe), [data.payables, data.pvVouchers, projs, fcs, arOpen, finance, cfg, today, lastDay, ov.pipe]);
  const lgBook  = React.useMemo(() => fundLgBook(data.guarantees, today, lastDay), [data.guarantees, today, lastDay]);
  const lgRoom  = fundLgRoom(lgBook, cfg);

  const baseEvs = React.useMemo(() => {
    const out = [];
    rcv.items.forEach(r => out.push({ date: r.date, amt: r.amt, kind: r.src === 'other' ? 'other' : 'ar' }));
    ov.pipe.forEach(r => { if (!r.off && r.group === 'signed') fundCustIn(r, today, lastDay, arOpen, cfg).forEach(e => out.push({ date: e.date, amt: e.amt, kind: 'ar' })); });
    view.cats.forEach(c => {
      const kind = c.cat === 2 ? 'proj' : 'fixed';
      c.lines.forEach(x => {
        if (fundIsDebtLine(x.l) || (jobsMode && fundIsJobLine(x.l))) return;
        const certain = bdafLocked(x.l, cm);
        F.forEach(mk => { const a = x.vals[mk]; if (a > 0.5) bdafSpread(x.l, mk, a, today).forEach(e => out.push({ date: e.date, amt: -e.amt, kind, certain })); });
      });
    });
    jobs.evs.forEach(e => { if (jobsMode || e.kind === 'wipIn') out.push(e); });
    debt.rows.forEach(d => d.evs.forEach(e => out.push(e)));
    return out;
  }, [rcv, view, debt, F, cm, today, ov.pipe, arOpen, lastDay, jobs, jobsMode]);
  const walk0 = React.useMemo(() => fundWalk(F, today, openAmt, buffer, baseEvs), [F, today, openAmt, buffer, baseEvs]);

  const scens = React.useMemo(() => fundScenarios(cfg).map(scen => {
    const alloc = fundAllocate(ov.pipe, cfg, scen, today, lastDay, { arOpen, lgRoom });
    const walk = fundWalk(F, today, openAmt, buffer, baseEvs.concat(alloc.evs));
    return { scen, alloc, walk, need: walk[walk.length - 1].needCum, firstShort: walk.find(r => r.need > 0.5) || null,
      lowest: walk.reduce((m, r) => (!m || r.min < m.min ? r : m), null) };
  }), [ov.pipe, cfg, today, lastDay, F, openAmt, buffer, baseEvs, arOpen, lgRoom]);
  const res = scens.find(s => s.scen.k === scenK) || scens[0];

  const totalOf = (keys) => walk0.reduce((s, r) => s + keys.reduce((a, k) => a + (r.sum[k] || 0), 0), 0);
  const totals = { out: -totalOf(['fixed', 'proj', 'principal', 'interest']), need: walk0[walk0.length - 1].needCum };
  const firstShort = walk0.find(r => r.need > 0.5) || null;
  const certainOut = -baseEvs.filter(e => e.amt < 0 && e.certain).reduce((s, e) => s + e.amt, 0);

  /* ยอดสัญญาที่ต้องเซ็น: จำลองสัญญาที่เซ็นวันเป้าหมาย ทั้งต้นทุนหลังเซ็น หลักประกัน เงินกู้ และเงินรับ */
  const lit = fundLender(cfg, 'lit'), alt = fundLender(cfg, 'alt');
  const signAt = cfg.targetDate > today ? cfg.targetDate : today;
  const lgPct = (Number(cfg.lgPct) || 0) / 100;
  const rcvCfg = Object.assign({}, FUND_RCV_DEFAULT, cfg.rcv);
  const contractFor = (shorts, used, lgLeft) => {
    const roomOf = (L) => (L.limit == null ? null : Math.max(0, L.limit - ((used && used[L.k]) || 0)));
    const run = (share) => fundNeedContract(shorts, fundUnit(cfg, lit, signAt, today, lastDay, share), roomOf(lit), fundUnit(cfg, alt, signAt, today, lastDay, share));
    let r = run(0);
    // วงเงิน LG ไม่พอสำหรับสัญญาทั้งก้อน → ส่วนที่เกินวางเป็นเงินสด แล้วคิดใหม่
    if (lgLeft != null && r.total * lgPct > lgLeft + 0.5) r = run(Math.max(0, lgPct - lgLeft / r.total));
    return r;
  };
  const shorts0 = fundShorts(baseEvs, openAmt, buffer, today);
  const need0 = contractFor(shorts0, {}, lgRoom);
  const moreNeed = contractFor(fundShorts(baseEvs.concat(res.alloc.evs), openAmt, buffer, today), res.alloc.used, res.alloc.lgLeft);
  const options = [lit, alt].filter(L => L.pre || L.post).map(L => {
    const u = fundUnit(cfg, L, signAt, today, lastDay, 0), r = fundNeedContract(shorts0, u, null, null);
    return { L, u, c: r.total, gross: r.total * u.gross, bridge: r.bridge, over: L.limit != null && r.total * u.gross > L.limit };
  });
  // เงินที่ยังขาดหลังเบิก ต้องเข้าก่อนวันเริ่มขาด → ย้อนหาวันเซ็นล่าสุดจากจำนวนวันเซ็นจนเงินเข้า
  const signBy = res.firstShort ? fundAddDays(res.firstShort.short, -lit.lead) : '';
  // สัญญา 100 ล้าน: เงินที่ต้องจ่ายในช่วงแผน · จุดที่ต้องหนุนสูงสุดถ้าไม่กู้
  const far = fundAddDays(signAt, 400);
  const per100 = { plan: {}, rcvPlan: 0 };
  fundUnit(cfg, null, signAt, today, lastDay, 0).evs.forEach(e => {
    if (e.kind === 'newCost') per100.plan[e.sub] = (per100.plan[e.sub] || 0) - e.amt * 1e8;
    if (e.kind === 'custIn') per100.rcvPlan += e.amt * 1e8;
  });
  const noLoan = fundTrough(fundUnit(cfg, null, signAt, today, far, 0), signAt);
  const noLoanCash = fundTrough(fundUnit(cfg, null, signAt, today, far, lgPct), signAt);
  const lenderCost = {};
  [lit, alt].forEach(L => { lenderCost[L.k] = fundCompare({ value: 1e8, signDate: signAt, deliverEst: fundAddDays(signAt, fundBuildDays(cfg)), stage: 'pre' }, L, cfg, today).total; });
  const cheaper = lenderCost.lit != null && lenderCost.alt != null ? (lenderCost.alt < lenderCost.lit ? 'alt' : 'lit') : null;
  const lgItems = res.alloc.items.filter(i => i.lgNeed > 0);
  const lgNeedPlan = lgItems.reduce((s, i) => s + i.lgNeed, 0), lgCashPlan = lgItems.reduce((s, i) => s + i.lgCash, 0);
  const lgIssuers = fundLgIssuers(cfg);
  const lgBanks = Array.from(new Set(Object.keys(lgBook.banks).concat(lgIssuers))).sort((a, b) => (cfg.lgIssue[b] ? 1 : 0) - (cfg.lgIssue[a] ? 1 : 0) || (lgBook.banks[b] || 0) - (lgBook.banks[a] || 0));
  const unconfirmed = fundUnconfirmed(cfg);
  const noCf = !plan.N;
  const pipeTotal = ov.pipe.filter(r => !r.off).reduce((s, r) => s + (Number(r.value) || 0), 0);
  const pipeCounted = ov.pipe.filter(r => !r.off && r.group !== 'unsure').reduce((s, r) => s + (Number(r.value) || 0), 0);
  const litUsed = (data.debtMaster || []).filter(m => m && String(m.status || '').trim() === 'Active' && (m.leasitSource === true || m.leasitSource === 'true')).reduce((s, m) => s + debtDisplayBalance(m), 0);
  const litOld = (data.debtMaster || []).filter(m => m && String(m.status || '').trim() === 'Active' && m.debtCategory === 'ลีซอิท' && !(m.leasitSource === true || m.leasitSource === 'true')).reduce((s, m) => s + debtDisplayBalance(m), 0);
  const litRates = (data.debtMaster || []).filter(m => m && String(m.status || '').trim() === 'Active' && (m.leasitSource === true || m.leasitSource === 'true') && Number(m.interestRate) > 0).map(m => Number(m.interestRate));
  const nettedDue = debt.rows.filter(d => d.inHz && d.treat === 'netted').reduce((s, d) => s + d.bal * (d.fx || 0), 0);
  const reviewRows = debt.rows.filter(d => d.treat === 'review');
  const reviewDue = reviewRows.reduce((s, d) => s + d.bal * (d.fx || 0), 0);
  const principalDue = -totalOf(['principal']);
  const rollDue = debt.rows.filter(d => d.inHz && !d.overdue && d.treat === 'roll').reduce((s2, d) => s2 + d.bal * (d.fx || 0), 0);
  const custCounted = totalOf(['ar', 'wipIn']);
  const fxCurs = Array.from(new Set(debt.rows.filter(d => d.cur !== 'THB').map(d => d.cur)));
  const apByMonth = {};
  fcs.forEach(f => {
    if (f.amount >= 0 || f.isActual || f.settled || !f.date || String(f.expType).toUpperCase() !== 'AP' || f.date < today) return;
    const mk = f.date.slice(0, 7);
    if (F.indexOf(mk) >= 0) apByMonth[mk] = (apByMonth[mk] || 0) - f.amount;
  });

  /* ── เขียนค่า ── */
  const saveCfg = (fn) => {
    if (!canEdit) return;
    WTPOverride.setRaw(FUND_CFG_KEY, JSON.stringify(fn(JSON.parse(JSON.stringify(ov.cfgRaw || {})))));
  };
  const setLender = (k, f, v) => saveCfg(o => { o.lenders = o.lenders || {}; o.lenders[k] = o.lenders[k] || {}; o.lenders[k][f] = v; return o; });
  const setLenderOk = (k, f, v) => saveCfg(o => { o.lenders = o.lenders || {}; o.lenders[k] = o.lenders[k] || {}; o.lenders[k].ok = Object.assign({}, o.lenders[k].ok, { [f]: v }); return o; });
  const setOpt = (k, v) => saveCfg(o => { o[k] = v; return o; });
  const setFx = (cur, v) => saveCfg(o => { o.fx = Object.assign({}, o.fx, { [cur]: v }); return o; });
  const setCost = (k, v) => saveCfg(o => { o.costs = Object.assign({}, o.costs); if (v == null) delete o.costs[k]; else o.costs[k] = v; return o; });
  const setRcv = (k, v) => saveCfg(o => { o.rcv = Object.assign({}, o.rcv, { [k]: v == null ? FUND_RCV_DEFAULT[k] : v }); return o; });
  const setLgIssue = (b, v) => saveCfg(o => { o.lgIssue = Object.assign({}, o.lgIssue, { [b]: v }); return o; });
  const setLgFree = (b, v) => saveCfg(o => { o.lgFree = Object.assign({}, o.lgFree, { [b]: v }); o.lgAsOf = today; return o; });   // null ต้องเก็บไว้ ไม่งั้นค่าเริ่มกลับมา
  const saveRow = (row) => {
    const id = row.id || ('r' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5));
    const rest = Object.assign({}, row);
    delete rest.id;
    WTPOverride.setRaw(FUND_PIPE + id, JSON.stringify(rest));
    setEditRow(null);
    if (toast) toast('บันทึก ' + row.name);
  };
  const delRow = (row) => {
    if (!window.confirm('ลบ “' + row.name + '” ออกจาก Pipeline?')) return;
    WTPOverride.setRaw(FUND_PIPE + row.id, '');
    setEditRow(null);
    if (toast) toast('ลบ ' + row.name);
  };
  const setRowDate = (r, k, v) => {
    const dates = Object.assign({}, r.dates);
    if (v) dates[k] = v; else delete dates[k];
    saveRow(Object.assign({}, r, { dates }));
  };
  const pickProject = (p) => {
    const wip = p.status === 'Work in progress', owed = wip && !!p.assignee;
    // โอนสิทธิไปแล้ว → สิ่งที่เข้าแผนคือเงินค้างรับหลังหักภาระหนี้ ไม่ได้เบิกเพิ่ม
    const debtAmt = owed ? Number((finance[p.contractNo] || {}).debt) || 0 : 0;
    saveRow({ name: p.site || p.name, code: p.contractNo || '', sysId: p.id,
      value: owed ? Math.max(0, p.outstandingAR - debtAmt) : (p.contractAmt || p.allocation || 0),
      group: wip ? 'signed' : p.projectStatus === 'ประกาศผู้ชนะ' ? 'soon' : 'approval',
      signDate: wip ? (p.start || '') : '', deliverDate: p.finish || '', assignable: owed ? 'yes' : '', lender: owed ? 'none' : 'auto', stage: 'pre',
      note: owed ? 'ค้างรับ ' + fundB(p.outstandingAR) + (debtAmt ? ' หักภาระหนี้ ' + fundB(debtAmt) : '') : '' });
    setOpen(o => Object.assign({}, o, { 3: true }));
  };
  const setDebtTreat = (d, v) => {
    if (!canEdit) return;
    const next = Object.assign({}, ov.debt);
    if (v === d.defTreat) delete next[d.id]; else next[d.id] = v;
    WTPOverride.setRaw(FUND_DEBT_KEY, Object.keys(next).length ? JSON.stringify(next) : '');
  };
  const toggleCheck = (i) => {
    if (!canEdit) return;
    const next = Object.assign({}, ov.check);
    if (next[i]) delete next[i]; else next[i] = bdafWho() || true;
    WTPOverride.setRaw(FUND_CHECK_KEY, Object.keys(next).length ? JSON.stringify(next) : '');
  };
  const doExport = () => {
    const ok = fundExportXlsx({ today, F, walk0, res, scens, need0, cfg, debt, alloc: scens[0].alloc, pipe: ov.pipe, totals, unconfirmed });
    if (toast) toast(ok ? 'ส่งออก Excel แล้ว' : 'ส่งออกไม่สำเร็จ');
  };

  /* ── ส่วนแสดงผล ── */
  const rangeLabel = bdafMShort(F[0]) + (F.length > 1 ? ' – ' + bdafMShort(F[F.length - 1]) : '');
  const byWhen = (d) => (d === today ? 'ตั้งแต่วันนี้' : 'ตั้งแต่ ' + bdafDShort(d));
  const numTd = { textAlign: 'right', fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' };
  const mRow = (label, vals, opt) => {
    const o = opt || {}, total = o.total;
    return (
      <tr className={o.cls || ''} onClick={o.onClick} style={{ cursor: o.onClick ? 'pointer' : undefined }}>
        <td style={{ fontWeight: o.bold ? 700 : 500, color: 'var(--ink-800)', paddingLeft: o.sub ? 26 : undefined }}>{label}</td>
        {vals.map((v, i) => <td key={i} style={{ ...numTd, fontWeight: o.bold ? 700 : 400, color: typeof v === 'number' && v < -0.5 ? 'var(--bad)' : undefined }}>{typeof v === 'number' ? (Math.abs(v) < 0.5 ? '–' : fundB(v)) : v}</td>)}
        {o.noTotal ? <td /> : <td style={{ ...numTd, fontWeight: 700 }}>{total == null ? '' : typeof total === 'number' ? fundB(total) : total}</td>}
      </tr>
    );
  };
  const sumKind = (keys) => walk0.map(r => keys.reduce((a, k) => a + (r.sum[k] || 0), 0));
  const tot = (arr) => arr.reduce((s, v) => s + v, 0);
  const catOf = (n) => view.cats.find(c => c.cat === n);
  const debtLines = view.cats.reduce((a, c) => a.concat(c.lines.filter(x => fundIsDebtLine(x.l))), []);
  const debtAvg = debtLines.reduce((s, x) => s + (x.l.avg || 0), 0);
  const finRest = (catOf(3) ? catOf(3).lines : []).filter(x => !fundIsDebtLine(x.l));
  const debtMain = debt.rows.filter(d => d.inHz || d.treat === 'review');
  const debtLater = debt.rows.filter(d => !d.inHz && d.treat !== 'review');
  const link = { border: 'none', background: 'none', color: 'var(--brand-600)', cursor: 'pointer', fontSize: 12, fontFamily: 'inherit', padding: 0 };
  const goBankDaily = () => { window.location.hash = '#bank_diary'; };
  const taken = new Set(ov.pipe.map(r => r.sysId).filter(Boolean));
  const groupSum = FUND_GROUPS.map(g => {
    const rows = ov.pipe.filter(r => !r.off && r.group === g.k);
    const ids = new Set(rows.map(r => r.id));
    return { g, n: rows.length, value: rows.reduce((s, r) => s + (Number(r.value) || 0), 0),
      net: scens[0].alloc.items.filter(i => ids.has(i.r.id)).reduce((s, i) => s + i.net, 0) };
  });
  const resKind = (keys) => res.walk.map(r => keys.reduce((a, k) => a + (r.sum[k] || 0), 0));
  const hzOpts = bdafRange(bdafAddM(cm, 2), bdafAddM(cm, 12));
  if (hzOpts.indexOf(hz) < 0) hzOpts.push(hz);
  const litItems = res.alloc.items.filter(i => i.L && i.L.k === 'lit').reduce((s, i) => s + i.net, 0);
  const altItems = res.alloc.items.filter(i => i.L && i.L.k === 'alt').reduce((s, i) => s + i.net, 0);
  const doneCount = FUND_CHECKLIST.filter((_, i) => ov.check[i]).length;
  const firstFund = scens[0].alloc.items.length;

  // เปิดหน้าใหม่: ตารางหนี้และ Cash Flow ยังมาจาก server ไม่ครบ ถ้าคำนวณจะขึ้น "เงินพอ" ผิดๆ → ยังไม่โชว์ตัวเลข
  const sync = WTPData.getSyncStatus ? WTPData.getSyncStatus() : null;
  if ((!(data.debtMaster || []).length && sync && !sync.time) || (cfState === 'loading' && !cf)) {
    return (
      <div className="page">
        <div className="page-head"><div><div className="page-title">แผนจัดหาเงินทุน</div></div></div>
        <div className="card" style={{ padding: '20px 16px', textAlign: 'center', fontSize: 13, color: 'var(--ink-500)' }}>กำลังโหลดข้อมูลหนี้และ Cash Flow…</div>
      </div>
    );
  }

  return (
    <div className="page">
      <style>{`
        .fund-tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
        .fund-tbl th { text-align: right; font-size: 12px; font-weight: 600; color: var(--ink-600); background: var(--ink-50); padding: 7px 10px; white-space: nowrap; border-bottom: 1px solid var(--line); }
        .fund-tbl th:first-child, .fund-tbl th.l { text-align: left; }
        .fund-tbl td { padding: 6px 10px; border-bottom: 1px solid var(--line-soft); }
        .fund-tbl tr.total td { background: #f3f8ff; border-top: 1.5px solid var(--brand-200); }
        .fund-tbl tr.gap td { background: #fff4f2; }
        .fund-tbl tr.ok td { background: #f0fdf4; }
        .fund-tbl tr.link:hover td { background: var(--brand-50); }
        .fund-wrap { overflow-x: auto; }
        .fund-note { padding: 9px 16px; font-size: 12px; color: var(--ink-500); line-height: 1.55; }
        @media (max-width: 900px) { .fund-kpis { grid-template-columns: 1fr 1fr !important; } }
        @media (max-width: 520px) { .fund-kpis { grid-template-columns: 1fr !important; } }
      `}</style>

      <div className="page-head">
        <div>
          <div className="page-title">แผนจัดหาเงินทุน</div>
          <div className="page-sub">{rangeLabel} · ต้องใช้เงินเท่าไร ต้องเซ็นสัญญากี่บาท เบิกจากที่ไหน เมื่อไร</div>
        </div>
        <div className="page-head-r" style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
          <label style={{ fontSize: 12.5, color: 'var(--ink-600)' }}>ดูถึง <select className="select input" disabled={!canEdit} value={hz} onChange={e => setOpt('endMonth', e.target.value)} style={{ padding: '3px 6px', fontSize: 12.5 }}>
            {hzOpts.map(mk => <option key={mk} value={mk}>{bdafMShort(mk)}</option>)}
          </select></label>
          <button className="btn" onClick={doExport}>ส่งออก Excel</button>
        </div>
      </div>

      {noCf && (
        <div className="card" style={{ padding: '12px 16px', marginBottom: 12, background: 'var(--warn-bg)', fontSize: 13 }}>
          ยังไม่มีข้อมูลจ่ายจริงจาก Cash Flow — ค่าใช้จ่ายประจำเป็น 0 ตัวเลขด้านล่างยังใช้ไม่ได้
        </div>
      )}

      <div className="fund-kpis" style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12, marginBottom: 12 }}>
        <FundKpi label={'ต้องใช้เงินรวม · ' + rangeLabel} value={fundM(totals.out)} sub={'จ่ายแน่นอน ' + fundM(certainOut) + ' · ประมาณการ ' + fundM(totals.out - certainOut)} />
        <FundKpi label="เงินทุนที่ยังขาด (Funding Gap)" value={fundM(totals.need)} tone={totals.need > 0.5 ? 'bad' : 'good'}
          sub={firstShort ? 'เริ่มขาด' + byWhen(firstShort.short).replace('ตั้งแต่', '') : 'เงินพอตลอดช่วงแผน'} />
        <FundKpi label="ยอดสัญญาที่ต้องเซ็น" value={fundM(need0.total)}
          sub={'รวมต้นทุนหลังเซ็นแล้ว · นับใน Pipeline ' + fundM(pipeCounted) + (moreNeed.total > 0.5 ? ' · ต้องเร่งเพิ่ม ' + fundM(moreNeed.total) : '') + (need0.bridge > 0.5 ? ' · ต้องมีเงินระยะสั้นคั่น ' + fundM(need0.bridge) : '')} />
        <FundKpi label={'วงเงินกู้สุทธิที่คาดว่าจะได้ · ' + res.scen.t} value={fundM(res.alloc.net)} tone={res.need > 0.5 ? 'bad' : res.alloc.net > 0 ? 'good' : undefined}
          sub={res.need > 0.5 ? 'เบิกแล้วยังขาด ' + fundM(res.need) : 'เบิกแล้วเงินพอ'} />
      </div>

      {/* ข้อสรุปสำหรับผู้บริหาร */}
      <div className="card" style={{ padding: '12px 16px', marginBottom: 12, fontSize: 13.5, lineHeight: 1.6, color: 'var(--ink-800)' }}>
        <div>
          {totals.need > 0.5
            ? <>ถ้าไม่หาเงินเพิ่ม เงินจะต่ำกว่าเงินสำรองขั้นต่ำ<b style={{ color: 'var(--bad)' }}>{byWhen(firstShort.short)}</b> ต้องมีเงินสุทธิเข้า <b>{fundM(totals.need)}</b></>
            : <>เงินพอถึง {bdafMShort(F[F.length - 1])} โดยไม่ต้องหาเงินเพิ่ม · ต่ำสุด {fundM(Math.min.apply(null, walk0.map(r => r.min)))}</>}
        </div>
        {ov.pipe.length > 0 ? (
          <div>
            แผน "{res.scen.t}" ใช้ {lit.name} {fundM(litItems)} · {alt.name} {fundM(altItems)}
            {res.need > 0.5
              ? <> แล้วยังขาด <b style={{ color: 'var(--bad)' }}>{fundM(res.need)}</b>{res.firstShort ? <> ({byWhen(res.firstShort.short)})</> : null}{moreNeed.total > 0.5 ? <> ต้องเซ็นเพิ่มอีกประมาณ <b>{fundM(moreNeed.total)}</b>{signBy >= today ? <> ภายใน <b>{bdafDShort(signBy)}</b> เงินถึงจะเข้าทัน</> : null}</> : null}{moreNeed.bridge > 0.5 ? <> · เซ็นเพิ่มก็ยังไม่ทัน ต้องมีเงินระยะสั้นมาคั่น <b>{fundM(moreNeed.bridge)}</b> ถึง {bdafDShort(moreNeed.bridgeTo)}</> : null}</>
              : <> เงินพอ · ต่ำสุด {fundM(res.lowest.min)} ({bdafDShort(res.lowest.minDate)})</>}
          </div>
        ) : <div style={{ color: 'var(--ink-500)' }}>ยังไม่มีโครงการใน Pipeline — ต้องเซ็นอย่างน้อย {fundM(need0.total)} (รวมต้นทุนหลังเซ็นและหลักประกันแล้ว) เพิ่มโครงการในข้อ 3 เพื่อดูว่าเบิกแล้วพอไหม</div>}
        <div style={{ marginTop: 6, fontSize: 12.5, color: '#92400e' }}>
          {!noCf && custCounted < custAvg * F.length * 0.5 && <div>เงินรับจากลูกค้านับได้แค่ {fundM(custCounted)} แต่ {plan.H.length} เดือนล่าสุดรับจริงเฉลี่ย {fundM(custAvg)}/เดือน — งวดที่ยังไม่ออกใบแจ้งหนี้ระบบไม่รู้ ให้ใส่ในข้อ 3 พร้อมวันลูกค้าจ่าย</div>}
          {!cfg.costOk && <div>ต้นทุนหลังเซ็นใช้ค่าจากประวัติ ({fundCostsOf(cfg).map(c => c.t.split(' ')[0] + ' ' + fmtNum(c.pct, 1) + '%').join(' · ')} · หลักประกัน {fmtNum(lgPct * 100, 1)}%) ยังไม่ยืนยันในข้อ 5</div>}
          {lgRoom == null && <div>ยังไม่รู้วงเงิน LG ของ {lgIssuers.join(' / ')} — ถ้าวงเงินไม่พอ สัญญาใหม่ต้องวางหลักประกันเป็นเงินสด {fmtNum(lgPct * 100, 1)}% ของมูลค่า ใส่วงเงินในข้อ 5</div>}
          {rollDue > 0.5 && <div>เงินต้นที่ครบกำหนด {fundM(rollDue)} นับว่าต่อสัญญา ไม่มีเงินออก{principalDue > 0.5 ? <> · ที่เลือกคืนจริง {fundM(principalDue)}</> : null} — สัญญาที่จะคืนจริงให้เปลี่ยนในข้อ 1</div>}
          {rollDue <= 0.5 && principalDue > 0.5 && <div>เงินต้นครบกำหนด {fundM(principalDue)} นับว่าคืนจริง สัญญาไหนจะต่อให้เปลี่ยนในข้อ 1</div>}
          {reviewRows.length > 0 && <div>{reviewRows.length} สัญญา {fundM(reviewDue)} เลยกำหนดแล้วแต่ยังเปิดอยู่ ยังไม่นับเงินต้น ตรวจในข้อ 1 ว่าคืน ต่อ หรือปิดไปแล้ว</div>}
          {Object.keys(debt.fxMissing).length > 0 && <div>ไม่รวมสัญญา {Object.keys(debt.fxMissing).join('/')} จนกว่าจะใส่อัตราแลกเปลี่ยนในข้อ 1</div>}
          {unconfirmed.length > 0 && <div>รอยืนยันเงื่อนไขสินเชื่อ {unconfirmed.length} ช่องในข้อ 4 ({unconfirmed.slice(0, 3).join(' · ')}{unconfirmed.length > 3 ? ' …' : ''})</div>}
        </div>
      </div>

      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 12, alignItems: 'center' }}>
        <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--ink-600)', marginRight: 4 }}>สถานการณ์</span>
        {scens.map(s => {
          const on = s.scen.k === res.scen.k;
          return <button key={s.scen.k} title={s.scen.hint} onClick={() => setScenK(s.scen.k)} style={{ padding: '5px 12px', borderRadius: 20, fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit', border: '1.5px solid ' + (on ? 'var(--brand-600)' : '#e2e8f0'), background: on ? 'var(--brand-600)' : '#fff', color: on ? '#fff' : '#475569' }}>{s.scen.t}</button>;
        })}
      </div>

      {/* 1 ค่าใช้จ่าย */}
      <FundStep no={1} title="ค่าใช้จ่ายที่ต้องจ่าย" open={open[1]} onToggle={() => tog(1)}
        sum={fundM(totals.out) + ' · จ่ายแน่นอน ' + fundM(certainOut) + ' · ภาระหนี้ ' + debt.rows.filter(d => d.pri > 0 || d.int > 0).length + ' สัญญา'}>
        <div className="fund-wrap">
          <table className="fund-tbl">
            <thead><tr><th>หมวด</th>{F.map(mk => <th key={mk}>{bdafMShort(mk)}{mk === cm ? ' (ที่เหลือ)' : ''}</th>)}<th>รวม</th></tr></thead>
            <tbody>
              {[[1, 'ดำเนินงาน'], [4, 'เงินเดือนและเบ็ดเตล็ด'], [3, 'ค่าธรรมเนียมและที่ปรึกษาแหล่งเงิน']].map(([n, t]) => {
                const c = catOf(n), lines = n === 3 ? finRest : (c ? c.lines : []);
                const vals = F.map(mk => lines.reduce((s, x) => s + (x.vals[mk] || 0), 0));
                return <React.Fragment key={n}>
                  {mRow(<>{detail['c' + n] ? '▾ ' : '▸ '}Fixed Cost · {t}</>, vals, { total: tot(vals), cls: 'link', onClick: () => setDetail(d => Object.assign({}, d, { ['c' + n]: !d['c' + n] })) })}
                  {detail['c' + n] && lines.map(x => <React.Fragment key={x.l.key}>{mRow(<>{x.l.name}{bdafLocked(x.l, cm) && <> <FundChip t="แน่นอน" tone="good" /></>}</>, F.map(mk => x.vals[mk]), { sub: true, total: x.total })}</React.Fragment>)}
                </React.Fragment>;
              })}
              {mRow('เงินต้นเงินกู้ที่ครบกำหนด', sumKind(['principal']).map(v => -v), { total: -tot(sumKind(['principal'])) })}
              {mRow('ดอกเบี้ย', sumKind(['interest']).map(v => -v), { total: -tot(sumKind(['interest'])) })}
              {(() => {
                const c = catOf(2), avgLines = c ? c.lines.filter(x => !(jobsMode && fundIsJobLine(x.l))) : [];
                const sub = (k) => F.map(mk => -baseEvs.filter(e => e.kind === 'proj' && e.sub === k && e.date.slice(0, 7) === mk).reduce((s2, e) => s2 + e.amt, 0));
                const ap = sub('ap'), wip = sub('wip');
                const vals = F.map((mk, i) => avgLines.reduce((s2, x) => s2 + (x.vals[mk] || 0), 0) + ap[i] + wip[i]);
                return <React.Fragment>
                  {mRow(<>{detail.c2 ? '▾ ' : '▸ '}ค่าใช้จ่ายโครงการเดิม</>, vals, { total: tot(vals), cls: 'link', onClick: () => setDetail(d => Object.assign({}, d, { c2: !d.c2 })) })}
                  {detail.c2 && jobsMode && mRow('AP ที่ตั้งแล้วยังไม่จ่าย (' + jobs.ap.n + ' ใบ)', ap, { sub: true, total: tot(ap) })}
                  {detail.c2 && jobsMode && mRow('ต้นทุนที่เหลือของโครงการที่กำลังทำ (' + jobs.wip.filter(w => w.left > 0.5).length + ' โครงการ)', wip, { sub: true, total: tot(wip) })}
                  {detail.c2 && avgLines.map(x => <React.Fragment key={x.l.key}>{mRow(<>{x.l.name}{bdafLocked(x.l, cm) && <> <FundChip t="แน่นอน" tone="good" /></>}</>, F.map(mk => x.vals[mk]), { sub: true, total: x.total })}</React.Fragment>)}
                </React.Fragment>;
              })()}
              {mRow('รวมต้องจ่าย', F.map((_, i) => -(sumKind(['fixed', 'proj', 'principal', 'interest'])[i])), { bold: true, cls: 'total', total: totals.out })}
            </tbody>
          </table>
        </div>
        <div className="fund-note">
          ค่าใช้จ่ายโครงการเดิมคิดจาก <select className="select input" disabled={!canEdit} value={jobsMode ? 'jobs' : 'avg'} onChange={e => setOpt('projMode', e.target.value)} style={{ padding: '2px 6px', fontSize: 12 }}>
            <option value="jobs">AP ที่ตั้งแล้ว + ต้นทุนที่เหลือของงานจริง</option>
            <option value="avg">ค่าเฉลี่ยย้อนหลัง</option>
          </select> · ประมาณค่าใช้จ่ายอื่นแบบ <select className="select input" disabled={!canEdit} value={mode} onChange={e => setOpt('expMode', e.target.value)} style={{ padding: '2px 6px', fontSize: 12 }}>
            <option value="auto">ไม่เอาเดือนที่จ่ายสูงผิดปกติ</option>
            <option value="avg">เฉลี่ยตรงๆ</option>
          </select> จาก <select className="select input" disabled={!canEdit} value={lookback} onChange={e => setOpt('lookback', Number(e.target.value))} style={{ padding: '2px 6px', fontSize: 12 }}>
            {BDAF_LOOKBACKS.map(n => <option key={n} value={n}>{n} เดือนล่าสุด</option>)}
          </select> · ยอดที่แก้หรือล็อกไว้ในการ์ด "แผนเงินสดล่วงหน้า" มีผลด้วย <button style={link} onClick={goBankDaily}>แก้ที่ Bank Daily</button> · แถวที่ล็อกไว้ = จ่ายแน่นอน
          {debtAvg > 0.5 && <> · เงินต้นและดอกเบี้ยใช้ตารางหนี้จริงแทนค่าเฉลี่ย (จ่ายจริงเฉลี่ย {fundM(debtAvg)}/เดือน)</>}
          {jobsMode && <> · AP ที่ตั้งแล้ว {jobs.ap.n} ใบ {fundM(jobs.ap.amt)} ลงตามแผนจ่ายหรือวันครบกำหนด (หักใบลดหนี้แล้ว){jobs.ap.late > 0.5 ? <> · <b style={{ color: 'var(--ink-800)' }}>เลยกำหนดเกิน 30 วัน {fundM(jobs.ap.late)}</b> ลงเป็นเงินออกวันนี้</> : null} · โครงการที่กำลังทำ {jobs.wip.length} โครงการ ต้นทุนที่เหลือ = สัดส่วนในข้อ 5 หักที่จ่ายแล้ว</>}
          {!jobsMode && Object.keys(apByMonth).length > 0 && <> · แผนจ่าย AP ที่ลงวันแล้ว {F.filter(mk => apByMonth[mk]).map(mk => bdafMShort(mk) + ' ' + fundM(apByMonth[mk])).join(' · ')} (รวมอยู่ในค่าประมาณแล้ว ไม่บวกซ้ำ)</>}
        </div>
        {debt.rows.length > 0 && (
          <div className="fund-wrap" style={{ borderTop: '1px solid var(--line)' }}>
            <table className="fund-tbl">
              <thead><tr><th>สัญญาที่ครบกำหนดหรือมีดอกเบี้ยในช่วงแผน</th><th className="l">เจ้าหนี้</th><th>ครบกำหนด</th><th>คงเหลือ</th><th>ดอกเบี้ย</th><th className="l">นับอย่างไร</th></tr></thead>
              <tbody>
                {(detail.debtAll ? debt.rows : debtMain).map(d => (
                  <tr key={d.id}>
                    <td>{d.no || '—'} <span style={{ fontSize: 11, color: 'var(--ink-500)' }}>{d.cat}</span></td>
                    <td style={{ maxWidth: 240, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{d.name}</td>
                    <td style={{ ...numTd, color: d.overdue ? 'var(--bad)' : undefined }}>{d.due ? fmtDate(d.due) : '—'}</td>
                    <td style={numTd}>{d.cur !== 'THB' ? d.cur + ' ' : ''}{fundB(d.bal)}</td>
                    <td style={numTd}>{fundB(d.int)}{d.est && <span title="ยังไม่มีตารางดอกเบี้ย คิดประมาณจากยอดคงเหลือ × อัตรา" style={{ color: 'var(--ink-400)' }}> ≈</span>}</td>
                    <td>{d.inHz
                      ? <select className="select input" disabled={!canEdit} value={d.treat} onChange={e => setDebtTreat(d, e.target.value)} style={{ padding: '3px 6px', fontSize: 12 }}>
                          {Object.keys(FUND_DEBT_TREAT).filter(k => k !== 'review' || d.overdue).map(k => <option key={k} value={k}>{FUND_DEBT_TREAT[k]}</option>)}
                        </select>
                      : <span style={{ fontSize: 12, color: 'var(--ink-500)' }}>ยังไม่ครบกำหนด</span>}</td>
                  </tr>
                ))}
              </tbody>
            </table>
            <div className="fund-note">
              {debtLater.length > 0 && <><button style={link} onClick={() => setDetail(d => Object.assign({}, d, { debtAll: !d.debtAll }))}>{detail.debtAll ? 'ซ่อนสัญญาที่ยังไม่ครบกำหนด' : 'แสดงสัญญาที่ยังไม่ครบกำหนด ' + debtLater.length + ' สัญญา'}</button> · </>}
              สัญญานักลงทุนที่ครบในช่วงแผนนับว่า <select className="select input" disabled={!canEdit} value={cfg.debtDefault === 'pay' ? 'pay' : 'roll'} onChange={e => setOpt('debtDefault', e.target.value)} style={{ padding: '2px 6px', fontSize: 12 }}>
                <option value="roll">ต่อสัญญาทั้งหมด</option>
                <option value="pay">คืนจริงทั้งหมด</option>
              </select> แล้วเปลี่ยนรายสัญญาในตาราง · ดอกเบี้ย WCI รวมจ่ายสิ้นไตรมาส ที่เหลือสิ้นเดือน · เช็คลีซอิทตามวันที่บนเช็ค · {nettedDue > 0.5 && <>เงินต้นสินเชื่อโอนสิทธิ์ที่ครบในช่วงแผน {fundM(nettedDue)} ไม่นับเป็นเงินออก เพราะหักจากยอดรับลูกค้าแล้ว ถ้าลูกค้ายังไม่จ่ายแต่ต้องคืนก่อนให้เปลี่ยนเป็น "คืนจริง" · </>}
              {fxCurs.map(cur => <span key={cur} style={{ marginRight: 12 }}>อัตราแลกเปลี่ยน {cur} <FundNum disabled={!canEdit} value={cfg.fx[cur]} onChange={v => setFx(cur, v)} placeholder="ยังไม่รู้" width={80} /> บาท {!cfg.fx[cur] && <FundChip t="ยังไม่รวมในยอด" tone="warn" />}</span>)}
            </div>
          </div>
        )}
      </FundStep>

      {/* 2 เงินสดและเงินที่ขาด */}
      <FundStep no={2} title="เงินสดและเงินที่ขาด (Funding Gap)" open={open[2]} onToggle={() => tog(2)}
        sum={totals.need > 0.5 ? 'ขาดสะสม ' + fundM(totals.need) + ' · ' + byWhen(firstShort.short) : 'เงินพอตลอดช่วงแผน'}>
        <div className="fund-wrap">
          <table className="fund-tbl">
            <thead><tr><th>รายการ</th>{F.map(mk => <th key={mk}>{bdafMShort(mk)}</th>)}<th>รวม</th></tr></thead>
            <tbody>
              {mRow('เงินสดต้นเดือน', walk0.map(r => r.open), { bold: true, noTotal: true })}
              {mRow('รับชำระจากลูกค้า', sumKind(['ar']), { total: tot(sumKind(['ar'])) })}
              {mRow('คาดรับจากโครงการที่กำลังทำ (ประมาณ)', sumKind(['wipIn']), { total: tot(sumKind(['wipIn'])) })}
              {mRow('รายรับอื่นที่แน่นอน', sumKind(['other']), { total: tot(sumKind(['other'])) })}
              {mRow('Fixed Cost', sumKind(['fixed']), { total: tot(sumKind(['fixed'])) })}
              {mRow('ชำระหนี้และดอกเบี้ย', sumKind(['principal', 'interest']), { total: tot(sumKind(['principal', 'interest'])) })}
              {mRow('ค่าใช้จ่ายโครงการเดิม', sumKind(['proj']), { total: tot(sumKind(['proj'])) })}
              {mRow('เงินสดก่อนจัดหาเงินทุน', walk0.map(r => r.close), { bold: true, cls: 'total', noTotal: true })}
              {mRow('จุดต่ำสุดในเดือน', walk0.map(r => <span title={bdafDShort(r.minDate)} style={{ color: r.min < buffer ? 'var(--bad)' : undefined }}>{fundB(r.min)} <span style={{ fontSize: 11, color: 'var(--ink-500)' }}>{bdafDShort(r.minDate)}</span></span>), { noTotal: true })}
              {mRow('Funding Gap รายเดือน', walk0.map(r => r.need), { bold: true, cls: 'gap', total: totals.need })}
              {mRow('Funding Gap สะสม', walk0.map(r => r.needCum), { bold: true, cls: 'gap', noTotal: true })}
              {mRow('เงินกู้สุทธิเข้า · ' + res.scen.t, res.walk.map(r => r.sum.fund || 0), { total: res.alloc.net })}
              {mRow('เงินรับจากโครงการใหม่', resKind(['custIn']), { total: tot(resKind(['custIn'])) })}
              {mRow('ต้นทุนโครงการใหม่', resKind(['newCost']), { total: tot(resKind(['newCost'])) })}
              {mRow('หลักประกันที่ต้องวางเป็นเงินสด', resKind(['lgCash']), { total: tot(resKind(['lgCash'])) })}
              {mRow('ดอกเบี้ยและคืนเงินกู้', resKind(['fundCost', 'fundRepay']), { total: tot(resKind(['fundCost', 'fundRepay'])) })}
              {mRow('เงินสดหลังจัดหาเงินทุน', res.walk.map(r => r.close), { bold: true, cls: res.need > 0.5 ? 'gap' : 'ok', noTotal: true })}
            </tbody>
          </table>
        </div>
        <div className="fund-note">
          ตั้งต้นจากยอดเงินใช้ได้ {fundB(openAmt)} บาท{opening.asOf ? ' ณ ' + fmtDate(opening.asOf) : ''} · เงินสำรองขั้นต่ำ {fundB(buffer)} บาท <button style={link} onClick={goBankDaily}>ตั้งที่ Bank Daily</button>
          {rcv.skip && (rcv.skip.insp.amt + rcv.skip.issue.amt + rcv.skip.nodate.amt) > 0 && <> · ยังไม่นับเงินรับที่รอใบตรวจรับ ติดปัญหา หรือไม่มีวันคาดรับ {fundM(rcv.skip.insp.amt + rcv.skip.issue.amt + rcv.skip.nodate.amt)}</>}
        </div>
      </FundStep>

      {/* 3 Pipeline */}
      <FundStep no={3} title="โครงการจากฝ่ายขาย (Contract Pipeline)" open={open[3]} onToggle={() => tog(3)}
        sum={ov.pipe.length ? ov.pipe.length + ' โครงการ · มูลค่า ' + fundM(pipeTotal) : 'ยังไม่มีโครงการ'}
        right={canEdit && <div style={{ display: 'flex', gap: 6 }}>
          <button className="btn btn-sm" onClick={() => setPicking(true)}>เลือกจากระบบ</button>
          <button className="btn btn-sm" onClick={() => setEditRow({ group: 'soon', lender: 'auto', stage: 'pre', assignable: '' })}>＋ เพิ่มเอง</button>
        </div>}>
        {ov.pipe.length === 0
          ? <div className="fund-note">ยังไม่มีโครงการ · เลือกจากโครงการรอลงนามในระบบ หรือเพิ่มตามข้อมูลจากฝ่ายขาย ต้องมีวันคาดเซ็นถึงจะวางแผนเบิกได้ · โครงการที่กำลังเก็บเงินแต่ยังไม่ออกใบแจ้งหนี้ เลือกจากระบบแล้วใส่วันลูกค้าจ่าย เงินรับจะเข้าแผน</div>
          : <div className="fund-wrap">
              <table className="fund-tbl">
                <thead><tr><th>โครงการ</th><th className="l">กลุ่ม</th><th>มูลค่าสัญญา</th><th>วันคาดเซ็น</th><th>ส่งมอบ</th><th>ลูกค้าจ่าย</th><th className="l">โอนสิทธิ</th><th className="l">แหล่งเงิน</th><th>เบิกสุทธิ</th></tr></thead>
                <tbody>
                  {ov.pipe.map(r => {
                    const g = FUND_GROUP_OF[r.group] || FUND_GROUPS[1];
                    const a = res.alloc.items.find(i => i.r.id === r.id) || scens[0].alloc.items.find(i => i.r.id === r.id);
                    return (
                      <tr key={r.id} className="link" style={{ cursor: 'pointer' }} onClick={() => setEditRow(r)}>
                        <td style={{ maxWidth: 280 }}><div style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.name}</div>{r.code && <div style={{ fontSize: 11, color: 'var(--ink-500)' }}>{r.code}</div>}</td>
                        <td><span style={{ fontSize: 11, fontWeight: 700, padding: '1px 7px', borderRadius: 4, background: g.bg, color: g.c, whiteSpace: 'nowrap' }}>{g.t}</span></td>
                        <td style={numTd}>{fundB(r.value)}</td>
                        <td style={numTd}>{r.signDate ? fmtDate(r.signDate) : <FundChip t="ยังไม่มีวันเซ็น" tone="warn" />}</td>
                        <td style={numTd}>{r.deliverDate ? fmtDate(r.deliverDate) : '—'}</td>
                        <td style={numTd}>{r.payDate ? fmtDate(r.payDate) : '—'}</td>
                        <td>{r.assignable === 'yes' ? <FundChip t="ได้" tone="good" /> : r.assignable === 'no' ? <FundChip t="ไม่ได้" tone="bad" /> : <FundChip t="ยังไม่รู้" tone="warn" />}</td>
                        <td style={{ whiteSpace: 'nowrap' }}>{a ? (a.L ? a.L.name : a.why) : <span style={{ color: 'var(--ink-500)' }}>ไม่นับในแผนนี้</span>}{a && a.cut && <> <FundChip t="วงเงินไม่พอ" tone="bad" /></>}</td>
                        <td style={numTd}>{a && a.net ? fundB(a.net) : '—'}</td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>}
      </FundStep>

      {/* 4 เงื่อนไขสินเชื่อ */}
      <FundStep no={4} title="เปรียบเทียบเงื่อนไขสินเชื่อ" open={open[4]} onToggle={() => tog(4)}
        sum={unconfirmed.length ? 'รอยืนยัน ' + unconfirmed.length + ' ช่อง' : 'ยืนยันครบแล้ว'}>
        <div className="fund-wrap">
          <table className="fund-tbl">
            <thead><tr><th>รายการ</th>{['lit', 'alt'].map(k => <th key={k} className="l">{canEdit
              ? <input className="input" value={cfg.lenders[k].name} onChange={e => setLender(k, 'name', e.target.value)} style={{ width: 150, padding: '3px 6px', fontSize: 12.5 }} />
              : cfg.lenders[k].name}</th>)}</tr></thead>
            <tbody>
              {FUND_LENDER_FIELDS.map(x => (
                <tr key={x.f}>
                  <td style={{ fontWeight: 500, color: 'var(--ink-800)' }}>{x.t}</td>
                  {['lit', 'alt'].map(k => {
                    const L = cfg.lenders[k], isOk = L.ok && L.ok[x.f];
                    return (
                      <td key={k}>
                        <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
                          <FundNum disabled={!canEdit} value={L[x.f]} onChange={v => setLender(k, x.f, v)} placeholder="ยังไม่รู้" width={x.f === 'limit' ? 130 : 76} />
                          <span style={{ fontSize: 11.5, color: 'var(--ink-500)' }}>{x.suffix}</span>
                          {x.f !== 'leadDays' && (
                            <label style={{ fontSize: 11.5, display: 'inline-flex', alignItems: 'center', gap: 3, color: isOk ? '#15803d' : '#92400e', cursor: canEdit ? 'pointer' : 'default' }}>
                              <input type="checkbox" disabled={!canEdit} checked={!!isOk} onChange={e => setLenderOk(k, x.f, e.target.checked)} />{isOk ? 'ยืนยันแล้ว' : 'รอยืนยัน'}
                            </label>
                          )}
                        </div>
                        {k === 'lit' && x.f === 'limit' && litUsed + litOld > 0 && <div style={{ fontSize: 11, color: 'var(--ink-500)', marginTop: 2 }}>เปิดอยู่ในระบบ {fundM(litUsed)}{litOld > 0.5 ? ' + หมวดลีซอิทเดิม ' + fundM(litOld) : ''}</div>}
                        {k === 'lit' && x.f === 'rate' && litRates.length > 0 && <div style={{ fontSize: 11, color: 'var(--ink-500)', marginTop: 2 }}>สัญญาลีซอิทในระบบ {fmtNum(Math.min.apply(null, litRates) * 100, 2)}–{fmtNum(Math.max.apply(null, litRates) * 100, 2)}%</div>}
                      </td>
                    );
                  })}
                </tr>
              ))}
              <tr className="total">
                <td style={{ fontWeight: 700 }}>ต้นทุนเงินกู้ทั้งโครงการ · สัญญา 100 ล้าน</td>
                {['lit', 'alt'].map(k => <td key={k}>{lenderCost[k] == null ? <FundChip t="ยังไม่รู้ค่าคอมหรือดอกเบี้ย" tone="warn" /> : <><b>{fundB(lenderCost[k])}</b>{cheaper === k && <> <FundChip t="คุ้มกว่า" tone="good" /></>}</>}</td>)}
              </tr>
            </tbody>
          </table>
        </div>
        <div className="fund-note">ต้นทุนเงินกู้ = ค่าคอมสินเชื่อ + ดอกเบี้ยจนลูกค้าจ่าย (ถัวเฉลี่ยราว {fundPayDays(cfg)} วันหลังเซ็น ตามงวดรับเงินในข้อ 5) · ช่องว่าง = ยังไม่รู้ และคิดเป็น 0 ไปก่อน · เซ็นจนเงินเข้าเริ่มที่ 14 วัน ถ้าต้องเปิดบัญชีร่วมให้บวกเพิ่ม</div>
        {ov.pipe.length > 0 && (
          <div className="fund-wrap" style={{ borderTop: '1px solid var(--line)' }}>
            <table className="fund-tbl">
              <thead>
                <tr><th rowSpan={2}>โครงการ</th><th colSpan={4} style={{ textAlign: 'center' }}>{lit.name}</th><th colSpan={4} style={{ textAlign: 'center' }}>{alt.name}</th></tr>
                <tr>{[0, 1].map(i => <React.Fragment key={i}><th>เบิกได้สูงสุด</th><th>สุทธิหลังค่าคอม</th><th>ต้นทุนเงินกู้</th><th>ได้เงินใน</th></React.Fragment>)}</tr>
              </thead>
              <tbody>
                {ov.pipe.filter(r => !r.off && r.assignable !== 'no').map(r => {
                  const a = fundCompare(r, lit, cfg, today), b = fundCompare(r, alt, cfg, today);
                  const best = a.total != null && b.total != null && a.gross > 0 && b.gross > 0 ? (b.total < a.total ? b : a) : null;
                  const cell = (c) => <React.Fragment>
                    <td style={numTd}>{fundB(c.gross)}</td><td style={numTd}>{fundB(c.net)}</td>
                    <td style={numTd}>{c.total == null ? <FundChip t="ยังไม่รู้" tone="warn" /> : fundB(c.total)}{best === c && <> <FundChip t="คุ้มกว่า" tone="good" /></>}</td>
                    <td style={numTd}>{c.days == null ? '—' : c.days + ' วัน'}</td>
                  </React.Fragment>;
                  return <tr key={r.id}><td style={{ maxWidth: 220, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.name}</td>{cell(a)}{cell(b)}</tr>;
                })}
              </tbody>
            </table>
            <div className="fund-note">เบิกได้สูงสุดรวม Pre และ Post แล้ว — {lit.name} Pre {fmtNum((lit.pre || 0) * 100, 0)}% แล้วเพิ่มเป็น Post {fmtNum((lit.post || 0) * 100, 0)}% = เบิกรวม {fmtNum((lit.post || 0) * 100, 0)}% ไม่ใช่ {fmtNum(((lit.pre || 0) + (lit.post || 0)) * 100, 0)}% · ต้นทุนเงินกู้คิดจนลูกค้าจ่าย (ไม่มีวันจ่าย = ส่งมอบ + 45 วัน หรือรอบเวลาจริง)</div>
          </div>
        )}
      </FundStep>

      {/* 5 ยอดสัญญา + เงินที่ต้องจ่ายหลังเซ็น + หลักประกัน */}
      <FundStep no={5} title="ยอดสัญญาที่ต้องเซ็น และเงินที่ต้องจ่ายหลังเซ็น" open={open[5]} onToggle={() => tog(5)}
        sum={'ต้องมีสัญญาอย่างน้อย ' + fundM(need0.total) + (moreNeed.total > 0.5 ? ' · ฝ่ายขายต้องเร่งเพิ่ม ' + fundM(moreNeed.total) : '') + ' · ต้นทุนหลังเซ็น ' + fmtNum(fundCostPct(cfg), 1) + '% + หลักประกัน ' + fmtNum(lgPct * 100, 1) + '%'}>
        <div className="fund-note" style={{ paddingBottom: 0 }}>
          <b style={{ color: 'var(--ink-800)' }}>เงินที่ต้องจ่ายหลังเซ็น ต่อสัญญา 100 ล้าน</b> ถ้าเซ็น {bdafDShort(signAt)} · ค่าเริ่มวัดจากโครงการที่เซ็น มิ.ย. 67 – มี.ค. 69 แก้ได้
          {!cfg.costOk && <> <FundChip t="รอยืนยัน" tone="warn" /></>}
        </div>
        <div className="fund-wrap">
          <table className="fund-tbl">
            <thead><tr><th>รายการ</th><th>% ของมูลค่าสัญญา</th><th className="l">จ่ายเมื่อไร</th><th>ภายใน {bdafMShort(F[F.length - 1])}</th><th>ทั้งโครงการ</th></tr></thead>
            <tbody>
              {fundCostsOf(cfg).map(c => (
                <tr key={c.k}>
                  <td>{c.t}</td>
                  <td style={numTd}><FundNum disabled={!canEdit} value={c.pct} onChange={v => setCost(c.k, v)} width={64} /> %</td>
                  <td style={{ fontSize: 12, color: 'var(--ink-600)' }}>ครึ่งแรกภายในวันที่ {fundHalfDay(c.curve)} หลังเซ็น</td>
                  <td style={numTd}>{fundB(per100.plan[c.k] || 0)}</td>
                  <td style={numTd}>{fundB(c.pct * 1e6)}</td>
                </tr>
              ))}
              <tr>
                <td>หลักประกันสัญญา</td>
                <td style={numTd}><FundNum disabled={!canEdit} value={cfg.lgPct} onChange={v => setOpt('lgPct', v == null ? 0 : v)} width={64} /> %</td>
                <td style={{ fontSize: 12, color: 'var(--ink-600)' }}>ตอนเซ็น · ใช้ LG ได้ไม่ต้องจ่ายเงินสด · ได้คืนเมื่อหมดประกัน</td>
                <td style={numTd}>{fundB(lgPct * 1e8)}</td>
                <td style={numTd}>{fundB(lgPct * 1e8)}</td>
              </tr>
              <tr className="total">
                <td style={{ fontWeight: 700 }}>เงินรับจากลูกค้า (หัก 1%)</td><td />
                <td style={{ fontSize: 12, color: 'var(--ink-600)', whiteSpace: 'nowrap' }}>
                  งวด 1 วันที่ <FundNum disabled={!canEdit} value={rcvCfg.d1} onChange={v => setRcv('d1', v)} width={52} /> หลังเซ็น ได้ <FundNum disabled={!canEdit} value={rcvCfg.p1} onChange={v => setRcv('p1', v)} width={48} />%
                  {' · '}งวดสุดท้ายอีก <FundNum disabled={!canEdit} value={rcvCfg.gap} onChange={v => setRcv('gap', v)} width={48} /> วัน ได้ {fmtNum(100 - (Number(rcvCfg.p1) || 0), 0)}%
                </td>
                <td style={numTd}>{fundB(per100.rcvPlan)}</td>
                <td style={numTd}>{fundB(1e8 * 106 / 107)}</td>
              </tr>
            </tbody>
          </table>
        </div>
        <div className="fund-note">
          ถ้าไม่กู้ สัญญา 100 ล้านต้องมีเงินหนุนสูงสุด <b style={{ color: 'var(--ink-800)' }}>{fundM(-noLoan.min * 1e8)}</b> ราววันที่ {noLoan.day} หลังเซ็น
          {lgPct > 0 && <> หรือ {fundM(-noLoanCash.min * 1e8)} ถ้าต้องวางหลักประกันเป็นเงินสด</>}
          {' · '}ของที่จ่ายล่วงหน้าแล้ว (เช่น มัดจำถัง) <FundNum disabled={!canEdit} value={cfg.stockPaid} onChange={v => setOpt('stockPaid', v)} placeholder="ไม่มี" width={120} /> บาท ใช้หักค่าของของโครงการใหม่ตามลำดับวันเซ็น
          {' · '}<label style={{ display: 'inline-flex', alignItems: 'center', gap: 4, cursor: canEdit ? 'pointer' : 'default', color: cfg.costOk ? '#15803d' : '#92400e' }}>
            <input type="checkbox" disabled={!canEdit} checked={!!cfg.costOk} onChange={e => setOpt('costOk', e.target.checked)} />{cfg.costOk ? 'ยืนยันสัดส่วนแล้ว' : 'ยังไม่ยืนยันสัดส่วน'}
          </label>
        </div>
        <div className="fund-wrap" style={{ borderTop: '1px solid var(--line)' }}>
          <table className="fund-tbl">
            <thead><tr><th>หลักประกันแบบ LG</th><th>ค้างในหน้าหลักประกัน</th><th className="l">ออกใบใหม่</th><th>วงเงินว่างตอนนี้</th></tr></thead>
            <tbody>
              {lgBanks.map(b => {
                const on = !!cfg.lgIssue[b], free = fundN(cfg.lgFree[b]), usedB = lgBook.banks[b] || 0;
                return (
                  <tr key={b}>
                    <td>{b}</td>
                    <td style={numTd}>{fundB(usedB)}</td>
                    <td>
                      <label style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 12, color: on ? 'var(--ink-800)' : 'var(--ink-500)', cursor: canEdit ? 'pointer' : 'default', whiteSpace: 'nowrap' }}>
                        <input type="checkbox" disabled={!canEdit} checked={on} onChange={e => setLgIssue(b, e.target.checked)} />{on ? 'ออกใบใหม่' : 'ค้ำงานเดิม ไม่ออกใบใหม่'}
                      </label>
                    </td>
                    <td style={numTd}>{on ? <FundNum disabled={!canEdit} value={free} onChange={v => setLgFree(b, v)} placeholder="ยังไม่รู้" width={130} /> : '—'}</td>
                  </tr>
                );
              })}
              <tr className="total">
                <td style={{ fontWeight: 700 }}>รวม</td>
                <td style={numTd}>{fundB(Object.keys(lgBook.banks).reduce((s, b) => s + lgBook.banks[b], 0))}</td><td />
                <td style={{ ...numTd, fontWeight: 700 }}>{lgRoom == null ? 'ยังไม่รู้' : !lgIssuers.length ? 'ไม่ออกใบใหม่' : fundB(lgRoom)}</td>
              </tr>
            </tbody>
          </table>
          <div className="fund-note">
            วงเงินว่าง ณ {bdafDShort(cfg.lgAsOf)} · วงเงินแบบ 1 ต่อ 1 ใช้หมดแล้วต้องวางเงินสดเท่ายอดหลักประกัน · สัญญาใหม่ในแผน "{res.scen.t}" ต้องใช้หลักประกัน {fundM(lgNeedPlan)}
            {lgNeedPlan > 0.5 && (lgRoom == null
              ? <> · <b style={{ color: '#92400e' }}>ยังไม่รู้วงเงิน LG</b> ยังไม่นับเงินสดที่อาจต้องวาง</>
              : lgCashPlan > 0.5 ? <> · วงเงิน LG ไม่พอ <b style={{ color: 'var(--bad)' }}>ต้องวางเงินสด {fundM(lgCashPlan)}</b></> : <> · ใช้ LG ได้ทั้งหมด</>)}
            {' · '}เงินสดค้ำที่วางอยู่ {fundM(lgBook.cash)} · ครบกำหนดในช่วงแผน LG {fundM(lgBook.lgSoon)} เงินสด {fundM(lgBook.cashSoon)}
            {(lgBook.lgLate + lgBook.cashLate) > 0.5 && <> · <b style={{ color: 'var(--ink-800)' }}>หมดประกันแล้วยังไม่ได้คืน LG {fundM(lgBook.lgLate)} เงินสด {fundM(lgBook.cashLate)}</b> เร่งตามคืนได้</>}
          </div>
        </div>
        <div className="fund-note" style={{ paddingBottom: 0, borderTop: '1px solid var(--line)' }}>
          ขาดสะสม <b style={{ color: 'var(--ink-800)' }}>{fundB(totals.need)}</b> บาท · ยอดสัญญาคิดให้ทุกวันที่เงินขาดกลับมาพอ หลังหักต้นทุนหลังเซ็น หลักประกัน ดอกเบี้ย และคืนเงินกู้เมื่อลูกค้าจ่าย
        </div>
        <div className="fund-wrap">
          <table className="fund-tbl">
            <thead><tr><th>ถ้าใช้</th><th>เบิกต่อสัญญา</th><th>ค่าคอมสินเชื่อ</th><th>ต้องมีสัญญา</th><th>ยอดเบิก</th><th className="l">วงเงิน</th></tr></thead>
            <tbody>
              {options.map(o => (
                <tr key={o.L.k}>
                  <td>{o.L.name} เพียงแหล่งเดียว</td>
                  <td style={numTd}>{fmtNum(o.u.gross * 100, 0)}%</td>
                  <td style={numTd}>{o.L.fee == null ? <FundChip t="ยังไม่รู้" tone="warn" /> : fmtNum(o.L.fee * 100, 2) + '%'}</td>
                  <td style={{ ...numTd, fontWeight: 700 }}>{o.c > 0.5 ? fundB(o.c) : '—'}</td>
                  <td style={numTd}>{o.c > 0.5 ? fundB(o.gross) : '—'}</td>
                  <td>{o.L.limit == null ? <FundChip t="ยังไม่รู้วงเงิน" tone="warn" /> : o.over ? <FundChip t={'เกินวงเงิน ' + fundM(o.L.limit)} tone="bad" /> : <FundChip t="พอ" tone="good" />}</td>
                </tr>
              ))}
              {need0.total > 0.5 && (
                <tr className="total">
                  <td style={{ fontWeight: 700 }}>{need0.c2 > 0.5 ? 'ใช้ ' + lit.name + ' เต็มวงเงิน แล้วเสริม ' + alt.name : 'ใช้ ' + lit.name}</td>
                  <td /><td />
                  <td style={{ ...numTd, fontWeight: 700 }}>{fundB(need0.total)}</td>
                  <td style={numTd}>{need0.c2 > 0.5 ? fundM(need0.c1 * (options[0] ? options[0].u.gross : 0)) + ' + ' + fundM(need0.c2 * (options[1] ? options[1].u.gross : 0)) : ''}</td>
                  <td />
                </tr>
              )}
            </tbody>
          </table>
        </div>
        {need0.bridge > 0.5 && <div className="fund-note" style={{ color: 'var(--bad)' }}>เซ็นวันที่ {bdafDShort(signAt)} ก็ยังไม่ทัน: ถึง {bdafDShort(need0.bridgeTo)} ต้องมีเงินระยะสั้นมาคั่นอย่างน้อย {fundM(need0.bridge)} เพราะต้นทุนและหลักประกันออกก่อนเงินกู้เข้า</div>}
        <div className="fund-wrap" style={{ borderTop: '1px solid var(--line)' }}>
          <table className="fund-tbl">
            <thead><tr><th>ฝ่ายขายมีอยู่แล้ว</th><th>โครงการ</th><th>มูลค่าสัญญา</th><th>เบิกสุทธิทันในช่วงแผน</th></tr></thead>
            <tbody>
              {groupSum.map(x => (
                <tr key={x.g.k}>
                  <td>{x.g.t}{x.g.k === 'unsure' && <span style={{ fontSize: 11, color: 'var(--ink-500)' }}> (ไม่นับในแผน)</span>}</td>
                  <td style={numTd}>{x.n}</td><td style={numTd}>{fundB(x.value)}</td><td style={numTd}>{fundB(x.net)}</td>
                </tr>
              ))}
              <tr className={scens[0].need > 0.5 ? 'gap' : 'ok'}>
                <td style={{ fontWeight: 700 }}>{scens[0].need > 0.5 ? 'ยังขาดเงินสุทธิ' : 'เงินพอตามแผน'}</td>
                <td /><td /><td style={{ ...numTd, fontWeight: 700 }}>{fundB(scens[0].need)}</td>
              </tr>
            </tbody>
          </table>
        </div>
      </FundStep>

      {/* 6 แผนเบิก */}
      <FundStep no={6} title="แผนเซ็นสัญญาและเบิกเงินกู้" open={open[6]} onToggle={() => tog(6)}
        sum={firstFund ? firstFund + ' โครงการ · ' + bdafDShort(cfg.targetDate) + ' เป้าหมาย · ' + bdafDShort(cfg.lastDate) + ' กำหนดสุดท้าย' : 'ยังไม่มีโครงการที่นับในแผน'}>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 14, alignItems: 'center', padding: '10px 16px', fontSize: 12.5, color: 'var(--ink-600)' }}>
          <label>เป้าหมายเซ็นสัญญา <input className="input" type="date" disabled={!canEdit} value={cfg.targetDate} onChange={e => setOpt('targetDate', e.target.value)} style={{ padding: '3px 6px' }} /></label>
          <label>กำหนดเซ็นสุดท้าย <input className="input" type="date" disabled={!canEdit} value={cfg.lastDate} onChange={e => setOpt('lastDate', e.target.value)} style={{ padding: '3px 6px' }} /></label>
        </div>
        {res.alloc.items.length > 0 && (
          <div className="fund-wrap">
            <table className="fund-tbl">
              <thead><tr><th>โครงการ</th><th className="l">ยื่นกับ</th><th>เซ็น</th><th>ยื่นเอกสาร</th><th>โอนสิทธิ</th><th>อนุมัติ</th><th>เงินเข้า</th><th>เงินสุทธิ</th><th className="l">หลักประกัน</th><th>ต้นทุนในช่วงแผน</th><th className="l">ความเสี่ยง</th></tr></thead>
              <tbody>
                {res.alloc.items.map(i => {
                  const r = i.r, m = fundMilestones(r, i.L, today);
                  const cash = i.cashDate || m.cash;
                  const flags = [];
                  if (!r.signDate) flags.push(['ยังไม่มีวันเซ็น', 'warn']);
                  else if (r.signDate > cfg.lastDate) flags.push(['เซ็นหลังกำหนดสุดท้าย', 'bad']);
                  else if (r.signDate > cfg.targetDate) flags.push(['เสี่ยงเบิกไม่ทันสิ้นปี', 'warn']);
                  if (cash && cash.slice(0, 4) > String(cfg.targetDate).slice(0, 4)) flags.push(['เงินเข้าหลังสิ้นปี', 'warn']);
                  if (cash && firstShort && cash > firstShort.short) flags.push(['เงินเข้าหลังวันเริ่มขาด', 'bad']);
                  if (r.assignable === '' || r.assignable == null) flags.push(['ยังไม่รู้ว่าโอนสิทธิได้', 'warn']);
                  if (i.cut) flags.push(['วงเงินไม่พอ', 'bad']);
                  if (i.lgCash > 0.5) flags.push(['ต้องวางเงินค้ำ', 'bad']);
                  const dateIn = (k, val) => <input className="input" type="date" disabled={!canEdit || !i.L} value={val || ''} title="แก้วันจริงได้ · ลบ = ใช้วันที่ระบบคิด" onChange={e => setRowDate(r, k, e.target.value)}
                    style={{ padding: '2px 4px', fontSize: 12, width: 124, fontWeight: r.dates && r.dates[k] ? 700 : 400 }} />;
                  return (
                    <tr key={r.id}>
                      <td style={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.name}</td>
                      <td style={{ whiteSpace: 'nowrap' }}>{i.L ? i.L.name : i.why}</td>
                      <td style={numTd}>{r.signDate ? fmtDate(r.signDate) : '—'}</td>
                      <td style={numTd}>{dateIn('submit', m.submit)}</td>
                      <td style={numTd}>{dateIn('assign', m.assign)}</td>
                      <td style={numTd}>{dateIn('approve', m.approve)}</td>
                      <td style={numTd}>{dateIn('cash', i.cashDate ? (r.dates && r.dates.cash) || i.cashDate : m.cash)}</td>
                      <td style={numTd}>{fundB(i.net)}</td>
                      <td>{!(i.lgNeed > 0) ? '—' : i.lgUnknown ? <FundChip t={'ยังไม่รู้วงเงิน · ' + fundB(i.lgNeed)} tone="warn" /> : i.lgCash > 0.5 ? <FundChip t={'เงินสด ' + fundB(i.lgCash)} tone="bad" /> : <FundChip t={'LG ' + fundB(i.lgNeed)} tone="good" />}</td>
                      <td style={numTd}>{i.costPlan > 0.5 ? fundB(i.costPlan) : '—'}</td>
                      <td>{flags.length ? flags.map(f => <FundChip key={f[0]} t={f[0]} tone={f[1]} />) : <FundChip t="ตามแผน" tone="good" />}</td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        )}
        <div className="fund-note">วันที่ระบบคิดแบ่งจากจำนวนวันเซ็นจนเงินเข้าในข้อ 4 · ยังไม่หักวันหยุดปลายปี ใส่วันจริงทับได้</div>
      </FundStep>

      {/* 7 แผนสำรอง */}
      <FundStep no={7} title="แผนสำรอง" open={open[7]} onToggle={() => tog(7)}
        sum={'แย่สุดขาด ' + fundM(Math.max.apply(null, scens.map(s => s.need)))}
        right={<span style={{ fontSize: 12, color: 'var(--ink-600)' }}>เงินเข้าช้า <FundNum disabled={!canEdit} value={cfg.delayDays} onChange={v => setOpt('delayDays', v == null ? 0 : v)} width={52} /> วัน</span>}>
        <div className="fund-wrap">
          <table className="fund-tbl">
            <thead><tr><th>สถานการณ์</th><th className="l">สมมติว่า</th><th>เงินกู้สุทธิเข้า</th><th>เงินต่ำสุด</th><th>เริ่มขาด</th><th>ต้องหาทดแทน</th></tr></thead>
            <tbody>
              {scens.map(s => (
                <tr key={s.scen.k} className={'link' + (s.scen.k === res.scen.k ? ' total' : '')} style={{ cursor: 'pointer' }} onClick={() => setScenK(s.scen.k)}>
                  <td style={{ fontWeight: 600 }}>{s.scen.t} <span style={{ fontSize: 11, color: 'var(--ink-500)' }}>{s.scen.en}</span></td>
                  <td style={{ fontSize: 12, color: 'var(--ink-600)' }}>{s.scen.hint}</td>
                  <td style={numTd}>{fundB(s.alloc.net)}</td>
                  <td style={numTd}>{fundB(s.lowest.min)} <span style={{ fontSize: 11, color: 'var(--ink-500)' }}>{bdafDShort(s.lowest.minDate)}</span></td>
                  <td style={numTd}>{s.firstShort ? bdafDShort(s.firstShort.short) : '—'}</td>
                  <td style={{ ...numTd, fontWeight: 700, color: s.need > 0.5 ? 'var(--bad)' : 'var(--good)' }}>{s.need > 0.5 ? fundB(s.need) : 'เงินพอ'}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </FundStep>

      <FundStep title={'เช็กลิสต์ติดตามงาน · ' + doneCount + '/' + FUND_CHECKLIST.length} open={open.check} onToggle={() => tog('check')}>
        <div style={{ padding: '6px 16px 12px' }}>
          {FUND_CHECKLIST.map((x, i) => (
            <label key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 0', fontSize: 13, color: 'var(--ink-800)', cursor: canEdit ? 'pointer' : 'default' }}>
              <input type="checkbox" disabled={!canEdit} checked={!!ov.check[i]} onChange={() => toggleCheck(i)} />
              <span style={{ flex: 1, textDecoration: ov.check[i] ? 'line-through' : 'none', color: ov.check[i] ? 'var(--ink-500)' : undefined }}>{x[0]}</span>
              <span style={{ fontSize: 11.5, color: 'var(--ink-500)' }}>{typeof ov.check[i] === 'string' ? ov.check[i] : x[1]}</span>
            </label>
          ))}
        </div>
      </FundStep>

      {editRow && <FundRowModal key={editRow.id || 'new'} row={editRow} cfg={cfg} canEdit={canEdit} onSave={saveRow} onDelete={delRow} onClose={() => setEditRow(null)} />}
      {picking && <FundPickModal data={data} taken={taken} onPick={pickProject} onClose={() => setPicking(false)} />}
    </div>
  );
}
