/* revimport.jsx — นำเข้าข้อมูลรายได้จากไฟล์ Excel ของฝ่ายวิศวะ (ปุ่ม "นำเข้าจากไฟล์ Excel" ในหน้า #revenue)
 * อ่านไฟล์ในเบราว์เซอร์ด้วย XLSX (vendor/xlsx.js) ตัวไฟล์ไม่ถูกส่งขึ้นเซิร์ฟเวอร์ ส่งเฉพาะตัวเลขที่ยืนยันแล้วไป POST /api/revenue/import
 * รู้จักชีตจากหัวตาราง ไม่ยึดชื่อชีตหรือตำแหน่งแถว:
 *   gop      หัว Code · Sales Price · Subcontractor · Factory · Materials · Commission · Consultants · IoT → รุ่น ราคา ต้นทุน 6 หมวด
 *   stages   บล็อก "ขั้นตอนงาน · ต้นทุนที่นำมาคำนวณ · % · รายได้ · ระยะเวลา · ลูกค้า · ที่ปรึกษา · ผู้รับเหมา" ใต้แถวชื่อรุ่น + Sales Price
 *            → งวดงาน (% จากสัดส่วนต้นทุน · ระยะเวลา = วันสะสมนับจากลงนาม) + เอกสารยืนยันงวด · บล็อกที่ไม่มีคอลัมน์ระยะเวลาข้ามไป
 *   monthly  หัว "เดือน" + คอลัมน์ "งบปี 25xx …" → รายได้ที่รับรู้รายเดือนของปีที่ขอ 1 แหล่งงบต่อคอลัมน์ (แหล่งงบแบบยอดรายเดือน)
 * ไฟล์รวม VAT (เดาจากหัวไฟล์ ผู้ใช้แก้ได้): ราคาขาย ต้นทุนต่อเครื่อง และยอดรายเดือน หาร 1.07
 * ต้นทุนของงวดในชีตขั้นรายได้ไม่รวม VAT อยู่แล้ว และใช้แค่คิดสัดส่วน % จึงไม่หาร */

const RVI_GOP_COLS = {
  code: /^(code|รหัส(รุ่น)?)$/i, name: /^(product|model|ชื่อรุ่น|รุ่น)/i, price: /sales\s*price|ราคาขาย/i,
  contractor: /sub-?\s*contract|ผู้รับเหมา/i, factory: /factory|โรงงาน/i, material: /material|วัสดุ/i,
  commission: /commission|คอมมิชชั่น|ค่าคอม/i, consultant: /consult|ที่ปรึกษา/i, iot: /^iot$/i, gop: /gop\s*\(?\s*%|%\s*gop/i,
};
const RVI_STAGE_COLS = {
  name: /^ขั้นตอน(งาน)?$/, cost: /ต้นทุน/, pct: /^%$/, days: /ระยะเวลา/, customer: /ลูกค้า/, consultant: /ที่ปรึกษา/, contractor: /ผู้รับเหมา/,
};
const RVI_MONTHS = [['ม.ค', 'มกรา', 'jan'], ['ก.พ', 'กุมภา', 'feb'], ['มี.ค', 'มีนา', 'mar'], ['เม.ย', 'เมษา', 'apr'], ['พ.ค', 'พฤษภา', 'may'], ['มิ.ย', 'มิถุนา', 'jun'],
  ['ก.ค', 'กรกฎา', 'jul'], ['ส.ค', 'สิงหา', 'aug'], ['ก.ย', 'กันยา', 'sep'], ['ต.ค', 'ตุลา', 'oct'], ['พ.ย', 'พฤศจิกา', 'nov'], ['ธ.ค', 'ธันวา', 'dec']];
const RVI_VAT = 1 + R.VAT_RATE;

const rviText = (v) => (v == null ? '' : String(v).replace(/\s+/g, ' ').trim());
/** ตัวเลขจากเซลล์: number ตรงๆ · "1,234.50" → 1234.5 · "12%" → 0.12 · "-" หรือว่าง → 0 · อ่านไม่ได้ = NaN */
const rviNum = (v) => {
  if (typeof v === 'number') return Number.isFinite(v) ? v : NaN;
  const s = rviText(v).replace(/[,\s฿]/g, '');
  if (s === '' || /^-+$/.test(s)) return 0;
  const n = Number(s.replace(/%$/, ''));
  return Number.isFinite(n) ? (s.endsWith('%') ? n / 100 : n) : NaN;
};
/** คีย์ไว้จับคู่ชื่อรุ่นในชีตงวดงานกับรุ่นในไฟล์ GOP: "POG DRINK + โรงเรือน" = "POG Drink + House" · "Drinks" = "Drink" */
const rviKey = (s) => rviText(s).toLowerCase().replace(/โรงเรือน/g, 'house').replace(/solar\s*cells?/g, 'solar').replace(/drinks/g, 'drink')
  .replace(/[^a-z0-9฀-๿+]/g, '');
/** เดือนจากเซลล์ "ม.ค. (JAN)" · "มกราคม" · "Jan" · 1–12 → 1–12 (0 = ไม่ใช่เดือน) */
const rviMonth = (v) => {
  if (typeof v === 'number') return Number.isInteger(v) && v >= 1 && v <= 12 ? v : 0;
  const s = rviText(v).toLowerCase().replace(/\s/g, '');
  return RVI_MONTHS.findIndex((ks) => ks.some((k) => s.startsWith(k) || s.includes('(' + k))) + 1;
};
/** ชื่องวดไม่เอาเลขลำดับนำหน้า: "1.งานฐานราก" / "3 งาน Commissioning Test" → "งานฐานราก" / "งาน Commissioning Test" */
const rviStageName = (s) => rviText(s).replace(/^\d{1,2}\s*[.)]\s*|^\d{1,2}\s+/, '');
const rviLc = (s) => String(s || '').trim().toLowerCase();

/** คอลัมน์ของแต่ละหัวข้อในแถวหัวตาราง (หัวข้อแรกที่ตรงได้คอลัมน์นั้น) */
function rviCols(row, defs) {
  const out = {};
  (row || []).forEach((v, c) => {
    const t = rviText(v);
    if (!t) return;
    const k = Object.keys(defs).find((key) => out[key] == null && defs[key].test(t));
    if (k) out[k] = c;
  });
  return out;
}

/** % ของงวด: จากต้นทุนถ้าครบทุกงวด ไม่งั้นจากคอลัมน์ % (0.25 หรือ 25 ก็ได้) ปรับให้รวม 100 พอดี */
function rviStagePcts(rows) {
  const byCost = R.pctFromCosts(rows.map((s) => s.cost));
  if (byCost) return byCost;
  const raw = rows.map((s) => (s.rawPct > 0 ? s.rawPct : 0));
  const total = raw.reduce((t, x) => t + x, 0);
  if (!(total > 0)) return rows.map(() => 0);
  let used = 0;
  return raw.map((x, i) => {
    const p = i === raw.length - 1 ? R.round2(100 - used) : R.round2((x / total) * 100);
    used = R.round2(used + p);
    return p;
  });
}

/** ชื่อรุ่นและราคาเหนือหัวตารางงวดงาน: แถวที่มีชื่อรุ่น (คอลัมน์เดียวกับ "ขั้นตอนงาน") + ราคา (ตัวเลขแรกที่มากกว่า 0) */
function rviBlockTitle(rows, r, nameCol) {
  for (let i = r - 1; i >= Math.max(0, r - 2); i--) {
    const row = rows[i] || [];
    const texts = row.map(rviText).filter((t) => t && !/sales\s*price|ราคาขาย|เอกสาร/i.test(t) && !/^[\d,.]+$/.test(t));
    const title = rviText(row[nameCol]) && !/sales\s*price/i.test(rviText(row[nameCol])) ? rviText(row[nameCol]) : texts[0] || '';
    const price = row.find((v) => typeof v === 'number' && v > 0) || 0;
    if (title && price) return { title, price };
  }
  return { title: '', price: 0 };
}

/** อ่าน 1 ชีต → { gop: [], blocks: [], months: [], text } */
function rviParseSheet(sheet, ws) {
  const rows = XLSX.utils.sheet_to_json(ws, { header: 1, raw: true, defval: null, blankrows: true });
  const out = { gop: [], blocks: [], months: [], text: rows.slice(0, 12).map((r) => (r || []).map(rviText).join(' ')).join(' ') };

  // ① ตารางรุ่น + ต้นทุน (ใช้ตารางแรกของชีต)
  for (let r = 0; r < rows.length; r++) {
    const g = rviCols(rows[r], RVI_GOP_COLS);
    if (g.code == null || g.price == null || R.COST_PARTS.filter((k) => g[k.key] != null).length < 4) continue;
    for (let i = r + 1; i < rows.length; i++) {
      const x = rows[i] || [];
      const code = rviText(x[g.code]);
      if (!code || RVI_GOP_COLS.code.test(code)) continue;
      const price = rviNum(x[g.price]);
      const costs = Object.fromEntries(R.COST_PARTS.map((k) => [k.key, g[k.key] == null ? 0 : rviNum(x[g[k.key]])]));
      const bad = !Number.isFinite(price) || Object.values(costs).some((v) => !Number.isFinite(v));
      if (!bad && !(price > 0) && !R.sum(Object.values(costs))) continue;          // แถวว่าง/แถวคั่น
      let gopPct = g.gop == null ? null : rviNum(x[g.gop]);
      if (gopPct != null && Number.isFinite(gopPct) && Math.abs(gopPct) <= 1.5) gopPct *= 100;
      out.gop.push({ sheet, row: i + 1, code, name: rviText(x[g.name]) || code, price, costs, gopPct: Number.isFinite(gopPct) ? gopPct : null, err: bad ? 'ตัวเลขไม่ถูกต้อง' : '' });
    }
    break;
  }

  // ② บล็อกงวดงาน (หลายบล็อกต่อชีต)
  for (let r = 0; r < rows.length; r++) {
    const c = rviCols(rows[r], RVI_STAGE_COLS);
    if (c.name == null || c.days == null || (c.cost == null && c.pct == null)) continue;
    const { title, price } = rviBlockTitle(rows, r, c.name);
    const list = [];
    for (let i = r + 1; i < rows.length; i++) {
      const x = rows[i] || [];
      const name = rviText(x[c.name]);
      if (!name || /^(รวม|total)/i.test(name)) break;
      const docs = { customer: c.customer == null ? '' : rviText(x[c.customer]), consultant: c.consultant == null ? '' : rviText(x[c.consultant]), contractor: c.contractor == null ? '' : rviText(x[c.contractor]) };
      list.push({
        name: rviStageName(name) || name, cost: c.cost == null ? null : rviNum(x[c.cost]), rawPct: c.pct == null ? 0 : rviNum(x[c.pct]),
        days: Math.round(rviNum(x[c.days])), docs: docs.customer || docs.consultant || docs.contractor ? docs : null,
      });
    }
    if (!list.length) continue;
    const pcts = rviStagePcts(list);
    const stages = list.map((s, i) => ({ name: s.name, pct: pcts[i], days: s.days, cost: s.cost > 0 ? R.round2(s.cost) : null, docs: s.docs }));
    const problem = stages.length > 20 ? 'เกิน 20 งวด' : stages.some((s) => !Number.isFinite(s.days)) ? 'ระยะเวลาไม่ใช่ตัวเลข' : R.stagesProblem(stages);
    out.blocks.push({ sheet, row: r + 1, title: title || 'ไม่มีชื่อรุ่น (แถว ' + (r + 1) + ')', price, stages, problem });
    r += list.length;
  }

  // ③ รายได้รายเดือน: คอลัมน์เดือน + คอลัมน์ "งบ…" แต่ละคอลัมน์ = 1 แหล่งงบ (ใช้ตารางแรกของชีต)
  for (let r = 0; r < rows.length; r++) {
    const row = rows[r] || [];
    const mc = row.findIndex((v) => /^(เดือน|month)/i.test(rviText(v)));
    if (mc < 0) continue;
    const cols = row.map((v, i) => ({ i, t: rviText(v) })).filter((x) => x.i !== mc && x.t && /งบ|budget|revenue|รายได้/i.test(x.t) && !/%/.test(x.t));
    if (!cols.length) continue;
    const vals = cols.map(() => ({ months: R.zeros(), seen: 0, bad: 0 }));
    for (let i = r + 1; i < rows.length && i <= r + 20; i++) {
      const x = rows[i] || [];
      if (/รวม|total/i.test(rviText(x[mc]))) break;
      const m = rviMonth(x[mc]);
      if (!m) continue;
      cols.forEach((col, k) => {
        const n = rviNum(x[col.i]);
        if (Number.isFinite(n)) vals[k].months[m - 1] = n; else vals[k].bad++;
        vals[k].seen++;
      });
    }
    cols.forEach((col, k) => {
      if (!vals[k].seen) return;
      const y = (col.t.match(/25\d\d/) || [])[0];
      out.months.push({
        sheet, col: col.i, title: col.t, name: col.t.replace(/\(\s*(19|20)\d\d\s*\)/g, '').replace(/\s+/g, ' ').trim().slice(0, 200),
        year: y ? Number(y) : null, months: vals[k].months, err: vals[k].bad ? 'มีเดือนที่ตัวเลขอ่านไม่ได้ ' + vals[k].bad + ' เดือน' : '',
      });
    });
    break;
  }
  return out;
}

/** อ่านไฟล์ → { name, vat, vatSeen, gop, blocks, months, sheets: [{ name, found }] } */
async function rviReadFile(file) {
  const wb = XLSX.read(await file.arrayBuffer(), { type: 'array' });
  const out = { name: file.name, gop: [], blocks: [], months: [], sheets: [] };
  let text = file.name;
  wb.SheetNames.forEach((sn) => {
    const p = rviParseSheet(sn, wb.Sheets[sn]);
    out.gop.push(...p.gop); out.blocks.push(...p.blocks); out.months.push(...p.months);
    const found = [p.gop.length && 'รุ่นและต้นทุน ' + p.gop.length + ' รุ่น', p.blocks.length && 'งวดงาน ' + p.blocks.length + ' แบบ',
      p.months.length && 'รายได้รายเดือน ' + p.months.length + ' คอลัมน์'].filter(Boolean).join(' · ');
    out.sheets.push({ name: sn, found });
    text += ' ' + sn + ' ' + p.text;
  });
  const excl = /exclud\w*\s*vat|ไม่รวม\s*(vat|ภาษี)|before\s*vat/i.test(text);
  out.vatSeen = !excl && /includ\w*\s*vat|รวม\s*vat|รวมภาษี/i.test(text);
  out.vat = !excl;          // ไม่มีคำบอก = ถือว่ารวม VAT (ไฟล์ของวิศวะรวม VAT) ผู้ใช้เอาติ๊กออกได้
  return out;
}

const RVI_STATUS = { new: { label: 'ใหม่', cls: 'ok' }, update: { label: 'อัปเดต', cls: 'cut' }, same: { label: 'ไม่เปลี่ยน', cls: '' } };

function RevenueImport({ data, onClose, onSaved }) {
  const year = data.round.year;
  const rights = data.rights;
  const [files, setFiles] = useState([]);
  const [reading, setReading] = useState(false);
  const [drag, setDrag] = useState(false);
  const [use, setUse] = useState({ gop: true, stages: true, monthly: true });
  const [match, setMatch] = useState({});          // บล็อกงวดงาน → รหัสรุ่น ('' = ไม่นำเข้า) ที่ผู้ใช้เลือกเอง
  const [srcEdit, setSrcEdit] = useState({});      // คอลัมน์รายเดือน → { name?, gopPct? } ที่ผู้ใช้แก้
  const [busy, setBusy] = useState(false);
  const seq = useRef(0);

  const addFiles = async (list) => {
    const picked = Array.from(list || []);
    if (!picked.length) return;
    setReading(true);
    const read = [];
    for (const f of picked) {
      if (!/\.xls[xmb]?$/i.test(f.name)) { toast(f.name + ': ไม่ใช่ไฟล์ Excel', 'error'); continue; }
      try {
        const r = await rviReadFile(f);
        if (!r.gop.length && !r.blocks.length && !r.months.length) toast(f.name + ': ไม่พบตารางที่ระบบรู้จัก', 'error');
        read.push({ id: ++seq.current, ...r });
      } catch (e) { toast(f.name + ': อ่านไฟล์ไม่ได้ (' + e.message + ')', 'error'); }
    }
    setFiles((cur) => [...cur.filter((x) => !read.some((n) => n.name === x.name)), ...read]);
    setReading(false);
  };
  const setFile = (id, patch) => setFiles((cur) => cur.map((f) => (f.id === id ? { ...f, ...patch } : f)));
  const ex = (v, vat) => R.round2(vat ? Number(v) / RVI_VAT : Number(v));

  // ── ① รุ่นและต้นทุน (รหัสซ้ำข้ามไฟล์: ใช้แถวแรก) ──
  const oldByCode = new Map(data.products.map((p) => [rviLc(p.code), p]));
  const gop = [];
  files.forEach((f) => f.gop.forEach((g) => {
    if (gop.some((x) => rviLc(x.code) === rviLc(g.code))) return;
    const price = ex(g.price, f.vat);
    const costs = Object.fromEntries(R.COST_PARTS.map((k) => [k.key, ex(g.costs[k.key], f.vat)]));
    const old = oldByCode.get(rviLc(g.code)) || null;
    const same = old && old.name === g.name && Number(old.price) === price && R.COST_PARTS.every((k) => Number(old.costs[k.key] || 0) === costs[k.key]);
    gop.push({ ...g, file: f, price, costs, old, status: !old ? 'new' : same ? 'same' : 'update' });
  }));
  const useGop = use.gop && gop.length > 0 && rights.project;

  // ── ② งวดงาน: รุ่นที่เลือกได้ = รุ่นในไฟล์ (ถ้านำเข้า) + รุ่นที่มีอยู่แล้ว ──
  const cand = new Map();
  if (useGop) gop.forEach((g) => cand.set(rviLc(g.code), { code: g.code, name: g.name, price: g.price, costs: g.costs, stages: g.old ? g.old.stages : [] }));
  data.products.forEach((p) => { if (!cand.has(rviLc(p.code))) cand.set(rviLc(p.code), p); });
  const candList = Array.from(cand.values());
  const blocks = files.flatMap((f) => f.blocks.map((b) => ({ ...b, file: f, key: f.id + ':' + b.sheet + ':' + b.row })));
  const autoCode = (b) => { const k = rviKey(b.title); const hit = candList.find((p) => rviKey(p.name) === k || rviKey(p.code) === k); return hit ? hit.code : ''; };
  const targetOf = (b) => { const code = b.key in match ? match[b.key] : autoCode(b); return code ? cand.get(rviLc(code)) || null : null; };
  const useStages = use.stages && blocks.length > 0 && rights.project;
  const tally = new Map();
  blocks.forEach((b) => { const t = targetOf(b); if (t) tally.set(rviLc(t.code), (tally.get(rviLc(t.code)) || 0) + 1); });
  const blockInfo = (b) => {
    const t = targetOf(b);
    const notes = [];
    if (t && tally.get(rviLc(t.code)) > 1) notes.push({ kind: 'err', text: 'มีแบบอื่นเลือกรุ่น ' + t.code + ' ด้วย — เลือกได้แบบเดียวต่อรุ่น' });
    if (b.problem) notes.push({ kind: 'skip', text: b.problem + ' — ข้ามแบบนี้' });
    if (t && b.price > 0 && Math.abs(ex(b.price, b.file.vat) - Number(t.price)) > 1) {
      notes.push({ kind: 'note', text: 'ราคาในชีตนี้ ' + fmt(ex(b.price, b.file.vat)) + ' ไม่ตรงกับราคารุ่น ' + fmt(t.price) });
    }
    if (!t) notes.push({ kind: 'skip', text: 'ไม่ได้เลือกรุ่น — ไม่นำเข้า' });
    return { t, notes, ok: !!t && !b.problem };
  };
  const stageCodes = new Set(useStages ? blocks.map(blockInfo).filter((x) => x.ok).map((x) => rviLc(x.t.code)) : []);
  const noStages = candList.filter((p) => !stageCodes.has(rviLc(p.code)) && R.stagesProblem(p.stages || []));

  // ── ③ รายได้รายเดือน: 1 คอลัมน์ = 1 แหล่งงบแบบยอดรายเดือน · GOP% ตั้งต้น = ของเดิม หรือค่าเฉลี่ย GOP% ของรุ่น ──
  const avgFrom = (useGop ? gop : data.products).filter((p) => Number(p.price) > 0).map((p) => ((Number(p.price) - R.unitCost(p)) / Number(p.price)) * 100);
  const avgGop = avgFrom.length ? R.round2(avgFrom.reduce((t, x) => t + x, 0) / avgFrom.length) : null;
  const oldSrc = new Map(data.sources.map((x) => [rviLc(x.name), x]));
  const sources = files.flatMap((f) => f.months.map((m) => {
    const key = f.id + ':' + m.sheet + ':' + m.col;
    const e = srcEdit[key] || {};
    const name = e.name != null ? e.name : m.name;
    const old = oldSrc.get(rviLc(name)) || null;
    return {
      key, file: f, sheet: m.sheet, title: m.title, name, err: m.err, year: m.year, old,
      signYear: m.year && m.year < year ? Math.max(m.year, year - 3) : null,
      monthly: m.months.map((v) => ex(v, f.vat)),
      gopPct: e.gopPct !== undefined ? e.gopPct : old && old.gopPct != null ? old.gopPct : avgGop,
      deals: old && old.mode !== 'monthly' ? data.deals.filter((d) => d.sourceId === old.id).length : 0,
    };
  }));
  const useMonthly = use.monthly && sources.length > 0 && rights.sales;
  const srcProblem = (s) => (!s.name.trim() ? 'ใส่ชื่อแหล่งงบ'
    : sources.some((o) => o !== s && rviLc(o.name) === rviLc(s.name)) ? 'ชื่อซ้ำกับคอลัมน์อื่น'
    : s.gopPct == null || !(s.gopPct >= -100 && s.gopPct <= 100) ? 'ใส่ % กำไรขั้นต้น' : s.err);

  // ── สิ่งที่จะส่ง ──
  const pOut = new Map();
  if (useGop) gop.forEach((g) => { if (g.status !== 'same') pOut.set(rviLc(g.code), { code: g.code, name: g.name, price: g.price, costs: g.costs }); });
  if (useStages) blocks.forEach((b) => {
    const { t, ok } = blockInfo(b);
    if (!ok) return;
    const base = pOut.get(rviLc(t.code)) || { code: t.code, name: t.name, price: t.price, costs: t.costs };
    pOut.set(rviLc(t.code), { ...base, stages: b.stages.map((s) => ({ name: s.name, pct: s.pct, days: s.days, cost: s.cost, docs: s.docs })) });
  });
  const sOut = useMonthly ? sources.map((s) => ({
    name: s.name.trim(), signYear: s.signYear, monthly: s.monthly, gopPct: s.gopPct, note: 'นำเข้าจากไฟล์ ' + s.file.name,
  })) : [];
  const errors = [
    ...(useGop ? gop.filter((g) => g.err).map((g) => 'รุ่น ' + g.code + ': ' + g.err) : []),
    ...(useStages ? blocks.flatMap((b) => blockInfo(b).notes.filter((n) => n.kind === 'err').map((n) => b.title + ': ' + n.text)) : []),
    ...(useMonthly ? sources.filter(srcProblem).map((s) => (s.name || s.title) + ': ' + srcProblem(s)) : []),
  ];
  const nNew = useGop ? gop.filter((g) => g.status === 'new').length : 0;
  const nUpd = useGop ? gop.filter((g) => g.status === 'update').length : 0;
  const summary = [nNew && 'รุ่นใหม่ ' + nNew, nUpd && 'อัปเดต ' + nUpd + ' รุ่น', stageCodes.size && 'งวดงาน ' + stageCodes.size + ' รุ่น',
    sOut.length && 'แหล่งงบรายเดือน ' + sOut.length].filter(Boolean).join(' · ');
  const canGo = !busy && !reading && (pOut.size > 0 || sOut.length > 0) && !errors.length;

  const go = async () => {
    setBusy(true);
    try {
      const r = await API.revenueImport({ products: Array.from(pOut.values()), sources: sOut });
      const done = [r.added && 'เพิ่ม ' + r.added + ' รุ่น', r.updated && 'อัปเดต ' + r.updated + ' รุ่น', r.stages && 'งวดงาน ' + r.stages + ' รุ่น',
        r.sourcesAdded && 'แหล่งงบใหม่ ' + r.sourcesAdded, r.sourcesUpdated && 'อัปเดตแหล่งงบ ' + r.sourcesUpdated].filter(Boolean);
      onSaved(r, 'นำเข้าแล้ว: ' + done.join(' · '));
    } catch (e) { toast(e.message, 'error'); } finally { setBusy(false); }
  };

  const sumMonths = sources.length ? R.zeros().map((_, i) => R.round2(R.sum(sources.map((s) => s.monthly[i])))) : [];
  return (
    <Modal title="นำเข้าข้อมูลรายได้จากไฟล์ Excel" wide="xxl" onClose={onClose} footer={<>
      <span className="small mute" style={{ marginRight: 'auto' }}>{errors.length ? <span className="err">แก้ {errors.length} จุดก่อนนำเข้า: {errors[0]}</span> : summary ? 'จะบันทึก: ' + summary : 'เลือกไฟล์ก่อน'}</span>
      <button className="btn" onClick={onClose}>ยกเลิก</button>
      <button className="btn primary" disabled={!canGo} onClick={go}><Icon name="upload" />{busy ? 'กำลังนำเข้า…' : 'นำเข้า'}</button>
    </>}>
      <label className={'rvi-drop' + (drag ? ' on' : '')}
        onDragOver={(e) => { e.preventDefault(); setDrag(true); }} onDragLeave={() => setDrag(false)}
        onDrop={(e) => { e.preventDefault(); setDrag(false); addFiles(e.dataTransfer.files); }}>
        <input type="file" accept=".xlsx,.xlsm,.xls" multiple onChange={(e) => { addFiles(e.target.files); e.target.value = ''; }} />
        <Icon name="upload" />
        <span><b>{reading ? 'กำลังอ่านไฟล์…' : files.length ? 'เพิ่มไฟล์อีก' : 'เลือกหรือลากไฟล์ Excel มาวางที่นี่'}</b>
          <span className="small mute">เลือกได้หลายไฟล์พร้อมกัน เช่น ไฟล์ GOP ของทุกรุ่น และไฟล์ประมาณการรายได้และขั้นการรับรู้รายได้ — ระบบดูหัวตารางเองว่าแต่ละชีตคืออะไร · ไฟล์อ่านในเครื่องนี้ ไม่ถูกอัปโหลด</span></span>
      </label>

      {files.map((f) => (
        <div key={f.id} className="rvi-file">
          <div className="grow">
            <b>{f.name}</b>
            <div className="small mute">{f.sheets.map((s) => s.name + ': ' + (s.found || 'ไม่ใช้')).join(' · ')}</div>
          </div>
          <label className="small rvi-vat" title="ราคาขาย ต้นทุนต่อเครื่อง และยอดรายเดือน หาร 1.07 · ต้นทุนของงวดงานไม่หาร (ใช้คิด % เท่านั้น)">
            <input type="checkbox" checked={f.vat} onChange={(e) => setFile(f.id, { vat: e.target.checked })} />
            ตัวเลขรวม VAT — หาร 1.07 ให้{f.vatSeen ? ' (หัวไฟล์ระบุว่ารวม VAT)' : ''}
          </label>
          <button className="btn ghost icon-only" aria-label={'เอาไฟล์ ' + f.name + ' ออก'} onClick={() => setFiles((cur) => cur.filter((x) => x.id !== f.id))}><Icon name="x" /></button>
        </div>
      ))}

      {gop.length > 0 && (
        <section className="rvi-sec">
          <label className="rvi-h">
            <input type="checkbox" checked={useGop} disabled={!rights.project} onChange={(e) => setUse({ ...use, gop: e.target.checked })} />
            <b>① รุ่นและต้นทุนต่อเครื่อง</b>
            <span className="small mute">{gop.length} รุ่น · ใหม่ {gop.filter((g) => g.status === 'new').length} · อัปเดต {gop.filter((g) => g.status === 'update').length} · ไม่เปลี่ยน {gop.filter((g) => g.status === 'same').length} · ไม่รวม VAT</span>
          </label>
          {!rights.project && <p className="small err">ส่วนนี้นำเข้าได้เฉพาะฝ่ายที่ดูแลต้นทุนโครงการและผู้ดูแล</p>}
          {useGop && (
            <div className="tw rvi-tw">
              <table className="t compact rvi-gop">
                <thead><tr><th>รหัส</th><th>รุ่น</th><th className="num">ราคาขาย</th><th className="num">ต้นทุน/เครื่อง</th><th className="num">GOP%</th><th /></tr></thead>
                <tbody>
                  {gop.map((g) => {
                    const c = R.unitCost(g), gp = g.price > 0 ? ((g.price - c) / g.price) * 100 : null;
                    const off = gp != null && g.gopPct != null && Math.abs(gp - g.gopPct) > 0.1;
                    return (
                      <tr key={g.code} className={g.err ? 'bad' : g.status === 'same' ? 'mute' : ''}>
                        <td><b>{g.code}</b></td><td>{g.name}</td>
                        <td className="num">{fmt(g.price)}{g.status === 'update' && Number(g.old.price) !== g.price && <div className="small mute">เดิม {fmt(g.old.price)}</div>}</td>
                        <td className="num">{fmt(c)}{g.status === 'update' && R.unitCost(g.old) !== c && <div className="small mute">เดิม {fmt(R.unitCost(g.old))}</div>}</td>
                        <td className={'num' + (gp < 0 ? ' red' : '')}>{gp == null ? '–' : gp.toFixed(1) + '%'}{off && <div className="small err">ในไฟล์ {g.gopPct.toFixed(1)}%</div>}</td>
                        <td>{g.err ? <span className="err small">{g.err}</span> : <span className={'tag ' + RVI_STATUS[g.status].cls}>{RVI_STATUS[g.status].label}</span>}</td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          )}
        </section>
      )}

      {blocks.length > 0 && (
        <section className="rvi-sec">
          <label className="rvi-h">
            <input type="checkbox" checked={useStages} disabled={!rights.project} onChange={(e) => setUse({ ...use, stages: e.target.checked })} />
            <b>② งวดงาน (ขั้นการรับรู้รายได้)</b>
            <span className="small mute">{blocks.length} แบบ · % คิดจากสัดส่วนต้นทุนของงวด · ระยะเวลา = วันนับจากลงนาม · งวดเดิมของรุ่นนั้นถูกแทนที่</span>
          </label>
          {!rights.project && <p className="small err">ส่วนนี้นำเข้าได้เฉพาะฝ่ายที่ดูแลต้นทุนโครงการและผู้ดูแล</p>}
          {useStages && (
            <div className="tw rvi-tw">
              <table className="t compact rvi-blocks">
                <thead><tr><th>แบบในไฟล์</th><th>ใส่ให้รุ่น</th><th>งวดงาน</th><th>หมายเหตุ</th></tr></thead>
                <tbody>
                  {blocks.map((b) => {
                    const { t, notes, ok } = blockInfo(b);
                    const last = b.stages[b.stages.length - 1];
                    return (
                      <tr key={b.key} className={ok ? '' : 'mute'}>
                        <td><b>{b.title}</b><div className="small mute">{b.sheet}{b.price ? ' · ราคา ' + fmt(ex(b.price, b.file.vat)) : ''}</div></td>
                        <td>
                          <select value={t ? t.code : ''} aria-label={'รุ่นสำหรับ ' + b.title} onChange={(e) => setMatch({ ...match, [b.key]: e.target.value })}>
                            <option value="">— ไม่นำเข้า —</option>
                            {candList.map((p) => <option key={p.code} value={p.code}>{p.code} · {p.name}</option>)}
                          </select>
                        </td>
                        <td>
                          <details className="rvi-st">
                            <summary>{b.stages.length} งวด · {last ? last.days : 0} วัน <span className="mute">({b.stages.map((s) => s.pct + '%').join(' · ')})</span></summary>
                            <ol>
                              {b.stages.map((s, i) => (
                                <li key={i}><b>{s.name}</b> <span className="mute">{s.pct}% · วันที่ {s.days}</span>
                                  {s.docs && <div className="small mute">{[s.docs.customer && 'ลูกค้า: ' + s.docs.customer, s.docs.consultant && 'ที่ปรึกษา: ' + s.docs.consultant, s.docs.contractor && 'ผู้รับเหมา: ' + s.docs.contractor].filter(Boolean).join(' · ')}</div>}
                                </li>
                              ))}
                            </ol>
                          </details>
                        </td>
                        <td className="small">{notes.map((n, i) => <div key={i} className={n.kind === 'err' ? 'err' : n.kind === 'note' ? 'rv-warn' : 'mute'}>{n.text}</div>)}</td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          )}
          {useStages && noStages.length > 0 && (
            <div className="banner yellow" style={{ margin: 0, boxShadow: 'none' }}>
              <Icon name="alert" />
              <div>หลังนำเข้า ยังมี <b>{noStages.length} รุ่นที่ไม่มีงวดงาน</b>: {noStages.map((p) => p.code).join(' · ')}
                <div className="small">ประมาณการขายแบบจำนวนเครื่องของรุ่นเหล่านี้ยังคำนวณรายได้ไม่ได้จนกว่าจะตั้งงวดงาน (แหล่งงบแบบยอดรายเดือนไม่ต้องใช้งวดงาน)</div></div>
            </div>
          )}
        </section>
      )}

      {sources.length > 0 && (
        <section className="rvi-sec">
          <label className="rvi-h">
            <input type="checkbox" checked={useMonthly} disabled={!rights.sales} onChange={(e) => setUse({ ...use, monthly: e.target.checked })} />
            <b>③ รายได้รายเดือน ปี {year}</b>
            <span className="small mute">1 คอลัมน์ = 1 แหล่งงบแบบยอดรายเดือน · ไม่รวม VAT · ต้นทุนงานก่อสร้าง = รายได้ × (100 − GOP%)</span>
          </label>
          {!rights.sales && <p className="small err">ส่วนนี้นำเข้าได้เฉพาะฝ่ายขายและผู้ดูแล</p>}
          {useMonthly && (
            <>
              <div className="rvi-srcs">
                {sources.map((s) => {
                  const p = srcProblem(s);
                  const tot = R.sum(s.monthly);
                  return (
                    <div key={s.key} className={'rvi-src' + (p ? ' bad' : '')}>
                      <div className="rvi-src-h">
                        <span className={'tag ' + (s.old ? 'cut' : 'ok')}>{s.old ? 'อัปเดต' : 'ใหม่'}</span>
                        <span className="small mute">คอลัมน์ "{s.title}" · {s.signYear ? 'งานยกมา ลงนามปี ' + s.signYear : 'งานปี ' + year}</span>
                      </div>
                      <label className="field">ชื่อแหล่งงบ
                        <input type="text" value={s.name} maxLength={200} onChange={(e) => setSrcEdit({ ...srcEdit, [s.key]: { ...srcEdit[s.key], name: e.target.value } })} />
                      </label>
                      <div className="rvi-src-n">
                        <label className="field">GOP%
                          <NumInput value={s.gopPct} placeholder="%" ariaLabel={'GOP% ของ ' + s.name} onValue={(x) => setSrcEdit({ ...srcEdit, [s.key]: { ...srcEdit[s.key], gopPct: x } })} />
                        </label>
                        <div className="rvkpi"><span>รายได้ทั้งปี</span><b>{fmt(tot)}</b></div>
                        <div className="rvkpi"><span>กำไรขั้นต้น</span><b>{s.gopPct == null ? '–' : fmt(R.round2(tot * s.gopPct / 100))}</b></div>
                      </div>
                      {avgGop != null && s.gopPct !== avgGop && (
                        <button type="button" className="btn ghost sm rvi-avg" onClick={() => setSrcEdit({ ...srcEdit, [s.key]: { ...srcEdit[s.key], gopPct: avgGop } })}>ใช้ค่าเฉลี่ย GOP% ของรุ่น {avgGop}%</button>
                      )}
                      {s.year && s.year > year && <div className="small rv-warn">ปีในหัวคอลัมน์ ({s.year}) หลังปีที่ขอ — ถือเป็นงานปี {year}</div>}
                      {s.deals > 0 && <div className="small rv-warn">แหล่งงบนี้มีประมาณการแบบจำนวนเครื่อง {s.deals} รายการ — หลังนำเข้าจะไม่นับ (ใช้ยอดรายเดือนแทน) แต่ไม่ลบ</div>}
                      {p && <div className="small err">{p}</div>}
                    </div>
                  );
                })}
              </div>
              <div className="tw rvi-tw">
                <table className="t compact rvi-months">
                  <thead><tr><th>เดือน</th>{sources.map((s) => <th key={s.key} className="num">{s.name || s.title}</th>)}{sources.length > 1 && <th className="num">รวม</th>}</tr></thead>
                  <tbody>
                    {R.MONTHS_TH.map((m, i) => (
                      <tr key={m}><td>{m} {String(year).slice(-2)}</td>
                        {sources.map((s) => <td key={s.key} className="num">{s.monthly[i] ? fmt(s.monthly[i]) : '–'}</td>)}
                        {sources.length > 1 && <td className="num"><b>{sumMonths[i] ? fmt(sumMonths[i]) : '–'}</b></td>}</tr>
                    ))}
                  </tbody>
                  <tfoot><tr><td>รวมทั้งปี</td>{sources.map((s) => <td key={s.key} className="num">{fmt(R.sum(s.monthly))}</td>)}{sources.length > 1 && <td className="num">{fmt(R.sum(sumMonths))}</td>}</tr></tfoot>
                </table>
              </div>
            </>
          )}
        </section>
      )}
    </Modal>
  );
}
