/* revenue.jsx — รายได้และต้นทุนงานโครงการ (#revenue)
 *   ประมาณการขาย (ฝ่ายขาย): แต่ละแหล่งงบเป็นตารางรายรุ่น ใส่จำนวน (ราคาต่อตัวตั้งต้นจากรุ่น) — เดือนลงนาม = ช่วงลงนามของแหล่งงบ
 *     กระจายเท่ากันทุกเดือนในช่วง · เพิ่มรายโครงการที่รู้เดือนลงนามแน่นอนแยกได้
 *   ผลิตภัณฑ์และงวดงาน (ฝ่ายที่ดูแลต้นทุนโครงการ): ราคาขาย ต้นทุนต่อเครื่อง 6 หมวด งวดงาน (% ของงาน · กี่วันหลังลงนาม)
 *   แหล่งงบแบบยอดรายเดือน (mode = monthly): รายได้ที่รับรู้รายเดือนตามไฟล์ประมาณการของวิศวะ + GOP% แทนตารางจำนวนเครื่อง
 *   สรุปรายเดือน: รายได้/ต้นทุนงานก่อสร้างที่รับรู้ตาม % ความสำเร็จของงาน (R.revenuePlan) — ชุดเดียวกับหน้างบกำไรขาดทุน
 *   นำเข้าจากไฟล์ Excel ของวิศวะ: RevenueImport ใน revimport.jsx
 * ราคาและต้นทุนเก็บแบบไม่รวม VAT (ช่องกรอกเลือกพิมพ์แบบรวม VAT ได้ ระบบหาร 1.07) · ระบบถือว่าลงนามวันที่ 15 ของเดือน */

const monthYear = (y, m) => R.MONTHS_TH[m - 1] + ' ' + String(y).slice(-2);
/** ช่วงลงนามของแหล่งงบเป็นข้อความ เช่น "ม.ค.–มี.ค. 70" */
const signWindow = (x, year) => {
  const y = x.signYear == null ? year : x.signYear;
  const m = x.signFrom === x.signTo ? R.MONTHS_TH[x.signFrom - 1] : R.MONTHS_TH[x.signFrom - 1] + '–' + R.MONTHS_TH[x.signTo - 1];
  return m + ' ' + String(y).slice(-2) + (y < year ? ' (งานยกมา)' : '');
};
/** แถวตาราง แหล่งงบ × รุ่น = รายการที่ไม่ได้ระบุเดือนลงนามเองและไม่มีชื่อโครงการ */
const isGridRow = (d) => d.signMonth == null && !d.customer;
const EMPTY_COSTS = Object.fromEntries(R.COST_PARTS.map((k) => [k.key, 0]));
const RV_VAT_DIV = 1 + R.VAT_RATE;
/** จำในเครื่องนี้ว่าผู้ใช้กรอกตัวเลขแบบรวม VAT ไหม (ตั้งต้น = รวม VAT ตามไฟล์ของวิศวะ) */
const rvVatPref = {
  get: () => { try { return localStorage.getItem('wpb-rv-vat') !== '0'; } catch (e) { return true; } },
  set: (on) => { try { localStorage.setItem('wpb-rv-vat', on ? '1' : '0'); } catch (e) { /* เก็บไม่ได้ก็ไม่เป็นไร */ } },
};
/** ค่าเฉลี่ย GOP% ของรุ่นที่มีราคา (ใช้เป็นค่าตั้งต้นของแหล่งงบแบบยอดรายเดือน) · ไม่มีรุ่น = null */
const rvAvgGop = (products) => {
  const list = products.filter((p) => Number(p.price) > 0).map((p) => ((Number(p.price) - R.unitCost(p)) / Number(p.price)) * 100);
  return list.length ? R.round2(list.reduce((t, x) => t + x, 0) / list.length) : null;
};

/** ช่องจำนวนเงินที่กรอกแบบรวม VAT ได้: value/onValue เป็นยอดไม่รวม VAT เสมอ · vat = รับตัวเลขรวม VAT แล้วบอกยอดไม่รวม VAT ใต้ช่อง */
function VatInput({ value, onValue, vat, placeholder, ariaLabel, nav }) {
  const [typed, setTyped] = useState(null);     // ตัวเลขรวม VAT ที่พิมพ์ล่าสุด กันปัดเศษไปกลับแล้วเพี้ยน 1 สตางค์
  if (!vat) return <NumInput value={value} onValue={onValue} placeholder={placeholder} ariaLabel={ariaLabel} nav={nav} />;
  const inc = value == null ? null : typed != null && R.round2(typed / RV_VAT_DIV) === Number(value) ? typed : R.round2(Number(value) * RV_VAT_DIV);
  return (
    <span className="vatin">
      <NumInput value={inc} placeholder={placeholder} ariaLabel={ariaLabel ? ariaLabel + ' (รวม VAT)' : undefined} nav={nav}
        onValue={(x) => { setTyped(x); onValue(x == null ? null : R.round2(x / RV_VAT_DIV)); }} />
      <span className="vatex">ไม่รวม VAT {value == null ? '–' : fmt(value)}</span>
    </span>
  );
}
/** ติ๊กเลือกกรอกแบบรวม VAT (ใช้ร่วมกันทุกฟอร์มในหน้านี้) */
function VatToggle({ vat, onChange }) {
  return (
    <label className="small vattoggle">
      <input type="checkbox" checked={vat} onChange={(e) => { rvVatPref.set(e.target.checked); onChange(e.target.checked); }} />
      กรอกตัวเลขแบบรวม VAT — ระบบหาร 1.07 แล้วเก็บเป็นยอดไม่รวม VAT
    </label>
  );
}

/** ช่วงเวลารับรู้ของ 1 งวด เทียบกับปีที่ขอ: before = รับรู้ไปแล้ว (งานยกมา) · this = ปีนี้ · after = ปีถัดไป */
const stageWhen = (st, year) => (st.year < year ? 'before' : st.year > year ? 'after' : 'this');
const WHEN_TAG = { before: { label: 'รับรู้ไปแล้ว', cls: '' }, this: { label: 'ปีนี้', cls: 'ok' }, after: { label: 'ปีถัดไป', cls: 'cut' } };

/** ตารางงวดงานของรายการขาย 1 รายการ: เสร็จเดือนไหน รับรู้เท่าไร */
function StagePreview({ deal, product, year }) {
  if (!product) return <p className="mute small">เลือกรุ่นเพื่อดูว่ารายได้จะรับรู้เดือนไหน</p>;
  const s = R.dealSchedule(deal, product, year);
  if (s.problem) return <div className="banner yellow" style={{ margin: 0, boxShadow: 'none' }}><Icon name="alert" /><div>รุ่น {product.code}: {s.problem} — ระบบยังคำนวณรายได้ของรายการนี้ไม่ได้</div></div>;
  return (
    <table className="t compact rvstages">
      <thead><tr><th>งวดงาน</th><th className="num">%</th><th>เสร็จ</th><th className="num">รายได้</th><th className="num">ต้นทุน</th><th /></tr></thead>
      <tbody>
        {s.stages.map((st, i) => {
          const w = stageWhen(st, year);
          return (
            <tr key={i} className={w === 'this' ? '' : 'mute'}>
              <td>{st.name}</td><td className="num">{st.pct}</td><td>{monthYear(st.year, st.month)}</td>
              <td className="num">{fmt(st.rev)}</td><td className="num">{fmt(st.cost)}</td>
              <td><span className={'tag ' + WHEN_TAG[w].cls}>{WHEN_TAG[w].label}</span></td>
            </tr>
          );
        })}
      </tbody>
      <tfoot><tr><td>รวม</td><td className="num">100</td><td /><td className="num">{fmt(s.value)}</td><td className="num">{fmt(s.cost)}</td><td /></tr></tfoot>
    </table>
  );
}

/** เพิ่ม/แก้รายการขาย 1 โครงการ (ฝ่ายขาย) */
function DealEditor({ data, deal, onClose, onSaved }) {
  const year = data.round.year;
  // แหล่งงบแบบยอดรายเดือนไม่นับรายการขาย จึงไม่ให้เลือก (เว้นรายการเดิมที่อยู่ในแหล่งนั้นอยู่แล้ว)
  const unitSources = data.sources.filter((x) => x.mode !== 'monthly' || (deal && deal.sourceId === x.id));
  const [f, setF] = useState(() => deal ? { ...deal } : {
    sourceId: unitSources[0] ? unitSources[0].id : null, customer: '', productId: data.products[0] ? data.products[0].id : null,
    qty: 1, price: null, signYear: year, signMonth: 1, note: '',
  });
  const [busy, setBusy] = useState(false);
  const set = (patch) => setF((x) => ({ ...x, ...patch }));
  const product = data.products.find((p) => p.id === Number(f.productId)) || null;
  const save = async () => {
    if (!product) { toast('เลือกรุ่นผลิตภัณฑ์', 'error'); return; }
    if (!(Number(f.qty) >= 1)) { toast('ใส่จำนวนเครื่อง', 'error'); return; }
    setBusy(true);
    try { onSaved(await API.saveDeal(deal && deal.id, { ...f, productId: product.id }), deal ? 'บันทึกแล้ว' : 'เพิ่มโครงการแล้ว'); }
    catch (e) { toast(e.message, 'error'); } finally { setBusy(false); }
  };
  const del = async () => {
    if (!window.confirm('ลบรายการขาย "' + (deal.customer || product.name) + '"?')) return;
    setBusy(true);
    try { onSaved(await API.deleteDeal(deal.id), 'ลบรายการแล้ว'); } catch (e) { toast(e.message, 'error'); } finally { setBusy(false); }
  };
  const years = [year - 2, year - 1, year];
  return (
    <Modal title={deal ? 'แก้รายการขาย' : 'เพิ่มโครงการขาย'} wide="xl" onClose={onClose} footer={<>
      {deal && <button className="btn danger" style={{ marginRight: 'auto' }} disabled={busy} onClick={del}><Icon name="trash" />ลบ</button>}
      <button className="btn" onClick={onClose}>ยกเลิก</button>
      <button className="btn primary" disabled={busy} onClick={save}><Icon name="check" />บันทึก</button>
    </>}>
      <div className="rvform">
        <label className="field">แหล่งงบ
          <select value={f.sourceId == null ? '' : f.sourceId} onChange={(e) => set({ sourceId: e.target.value === '' ? null : Number(e.target.value) })}>
            {unitSources.map((x) => <option key={x.id} value={x.id}>{x.name}{x.mode === 'monthly' ? ' (ยอดรายเดือน — รายการนี้ไม่นับ)' : ''}</option>)}
            <option value="">ไม่ระบุแหล่งงบ</option>
          </select>
        </label>
        <label className="field grow2">ลูกค้า / หน่วยงาน / ชื่อโครงการ
          <input type="text" value={f.customer} maxLength={200} placeholder="เช่น อบต. … จ.… (ไม่บังคับ)" onChange={(e) => set({ customer: e.target.value })} />
        </label>
        <label className="field grow2">รุ่น
          <select value={f.productId || ''} onChange={(e) => set({ productId: Number(e.target.value) })}>
            {data.products.map((p) => <option key={p.id} value={p.id}>{p.code} · {p.name} · {fmt(p.price)}</option>)}
          </select>
        </label>
        <label className="field">จำนวนเครื่อง
          <NumInput value={f.qty} onValue={(x) => set({ qty: x == null ? null : Math.round(x) })} placeholder="0" ariaLabel="จำนวนเครื่อง" />
        </label>
        <label className="field">ราคาต่อเครื่อง (ไม่รวม VAT)
          <NumInput value={f.price} onValue={(x) => set({ price: x })} placeholder={product ? 'ตามรุ่น ' + fmt(product.price) : ''} ariaLabel="ราคาต่อเครื่อง" />
        </label>
        <div className="field">คาดว่าลงนามสัญญา
          <span className="period">
            <MonthPick value={f.signMonth} label="เดือนที่ลงนาม" onChange={(m) => set({ signMonth: m })} />
            <select value={f.signYear} onChange={(e) => set({ signYear: Number(e.target.value) })} aria-label="ปีที่ลงนาม">
              {years.map((y) => <option key={y} value={y}>{y}{y < year ? ' (งานยกมา)' : ''}</option>)}
            </select>
          </span>
        </div>
        <label className="field full">หมายเหตุ
          <input type="text" value={f.note} maxLength={1000} placeholder="เช่น รอผลอนุมัติงบ · ส่วนลดพิเศษ" onChange={(e) => set({ note: e.target.value })} />
        </label>
      </div>
      <div className="rvpreview">
        <div className="small"><b>มูลค่าสัญญา {fmt(R.round2((Number(f.qty) || 0) * (f.price == null ? (product ? product.price : 0) : f.price)))} บาท</b>
          <span className="mute"> · ระบบถือว่าลงนามวันที่ {R.SIGN_DAY} {R.MONTHS_TH[(f.signMonth || 1) - 1]} {f.signYear} แล้วนับวันของแต่ละงวดจากวันนั้น</span></div>
        <StagePreview deal={f} product={product} year={year} />
      </div>
    </Modal>
  );
}

/** เพิ่ม/แก้แหล่งงบ (ฝ่ายขาย): ชื่อ เป้า และวิธีคิดรายได้
 *   units   = ใส่จำนวนเครื่องในตาราง ยอดกระจายเท่ากันทุกเดือนในช่วงลงนาม แล้วรับรู้ตามงวดงานของรุ่น
 *   monthly = ใส่รายได้ที่รับรู้แต่ละเดือนของปีที่ขอ (คิดตามงวดงานมาแล้ว เช่นจากไฟล์ประมาณการของวิศวะ) + GOP% */
function SourceEditor({ data, source, onClose, onSaved }) {
  const year = data.round.year;
  const [f, setF] = useState(() => source ? { ...source, signYear: source.signYear == null ? year : source.signYear, mode: source.mode || 'units' }
    : { name: '', target: null, note: '', signYear: year, signFrom: 1, signTo: 3, mode: 'units', monthly: null, gopPct: null });
  const [busy, setBusy] = useState(false);
  const [vat, setVat] = useState(rvVatPref.get);
  const used = source ? data.deals.filter((d) => d.sourceId === source.id).length : 0;
  const monthly = f.mode === 'monthly';
  const months = f.monthly || R.zeros();
  const avg = rvAvgGop(data.products);
  const plan = R.sourceMonthlyPlan({ monthly: months, gopPct: f.gopPct });
  const run = async (fn, msg) => { setBusy(true); try { onSaved(await fn(), msg); } catch (e) { toast(e.message, 'error'); } finally { setBusy(false); } };
  const save = () => {
    if (monthly && f.gopPct == null) { toast('ใส่ % กำไรขั้นต้น (GOP%) ของแหล่งงบนี้', 'error'); return; }
    run(() => API.saveSource(source && source.id, { ...f, monthly: monthly ? months : f.monthly }), 'บันทึกแหล่งงบแล้ว');
  };
  const del = () => {
    if (used && !window.confirm('ลบแหล่งงบ "' + source.name + '" พร้อมประมาณการขาย ' + used + ' รายการในแหล่งงบนี้?')) return;
    run(() => API.deleteSource(source.id, used > 0), 'ลบแหล่งงบแล้ว');
  };
  const yearPick = (label) => (
    <select value={f.signYear} onChange={(e) => setF({ ...f, signYear: Number(e.target.value) })} aria-label={label}>
      {[year, year - 1, year - 2].map((y) => <option key={y} value={y}>{y}{y < year ? ' (งานยกมา)' : ''}</option>)}
    </select>
  );
  return (
    <Modal title={source ? 'แก้แหล่งงบ' : 'เพิ่มแหล่งงบ'} wide="xl" onClose={onClose} footer={<>
      {source && <button className="btn danger" style={{ marginRight: 'auto' }} disabled={busy} onClick={del}><Icon name="trash" />ลบ</button>}
      <button className="btn" onClick={onClose}>ยกเลิก</button>
      <button className="btn primary" disabled={busy || !f.name.trim()} onClick={save}><Icon name="check" />บันทึก</button>
    </>}>
      <label className="field">ชื่อแหล่งงบ
        <input type="text" autoFocus value={f.name} maxLength={200} placeholder="เช่น งบประจำปี 70 · งบภัยแล้ง · งบเหลือจ่าย" onChange={(e) => setF({ ...f, name: e.target.value })} />
      </label>
      <div className="field rvmodef">วิธีคิดรายได้
        <span className="seg lg rvmode" role="radiogroup" aria-label="วิธีคิดรายได้">
          {R.SOURCE_MODES.map((m) => (
            <button key={m.key} type="button" role="radio" aria-checked={f.mode === m.key} className={f.mode === m.key ? 'on' : ''}
              onClick={() => setF({ ...f, mode: m.key, gopPct: m.key === 'monthly' && f.gopPct == null ? avg : f.gopPct })}>{m.label}</button>
          ))}
        </span>
        <span className="small mute">{monthly
          ? 'ใส่รายได้ที่รับรู้ในแต่ละเดือนของปี ' + year + ' (คิดตามงวดงานมาแล้ว) และ % กำไรขั้นต้น — ไม่ต้องใส่จำนวนเครื่อง'
          : 'ใส่จำนวนเครื่องของแต่ละรุ่นในตาราง ระบบคิดรายได้ตามงวดงานของรุ่นให้'}</span>
      </div>

      {!monthly && (
        <div className="field">ช่วงลงนามสัญญา
          <span className="period">
            <MonthPick value={f.signFrom} label="เดือนแรกที่ลงนาม" onChange={(m) => setF({ ...f, signFrom: m, signTo: Math.max(m, f.signTo) })} />
            <span>ถึง</span>
            <MonthPick value={f.signTo} label="เดือนสุดท้ายที่ลงนาม" onChange={(m) => setF({ ...f, signTo: m, signFrom: Math.min(m, f.signFrom) })} />
            {yearPick('ปีที่ลงนาม')}
          </span>
          <span className="small mute">ยอดที่กรอกในตารางของแหล่งงบนี้ ระบบแบ่งเท่ากันทุกเดือนในช่วงนี้ แล้วนับวันของงวดงานจากวันที่ {R.SIGN_DAY} ของแต่ละเดือน</span>
        </div>
      )}

      {monthly && (
        <>
          <div className="rvmhead">
            <div className="field">งบของปี {yearPick('งบของปี')}</div>
            <VatToggle vat={vat} onChange={setVat} />
          </div>
          <div className="rvmonths">
            {R.MONTHS_TH.map((m, i) => (
              <label key={m} className="field">{m} {String(year).slice(-2)}
                <VatInput vat={vat} value={months[i] || null} nav="srcm" placeholder="0" ariaLabel={'รายได้ ' + m}
                  onValue={(x) => setF({ ...f, monthly: months.map((v, k) => (k === i ? x || 0 : v)) })} />
              </label>
            ))}
          </div>
          <div className="rvcosts">
            <label className="field">% กำไรขั้นต้น (GOP%)
              <NumInput value={f.gopPct} onValue={(x) => setF({ ...f, gopPct: x })} placeholder="%" ariaLabel="% กำไรขั้นต้น" />
              {avg != null && f.gopPct !== avg && <button type="button" className="btn ghost sm rvi-avg" onClick={() => setF({ ...f, gopPct: avg })}>ใช้ค่าเฉลี่ยของรุ่น {avg}%</button>}
            </label>
            <div className="rvkpi"><span>รายได้ทั้งปี (ไม่รวม VAT)</span><b>{fmt(R.sum(plan.rev))}</b></div>
            <div className="rvkpi"><span>ต้นทุนงานก่อสร้าง</span><b>{fmt(R.sum(plan.cogs))}</b><em>รายได้ × (100 − GOP%)</em></div>
            <div className="rvkpi"><span>กำไรขั้นต้น</span><b>{fmt(R.round2(R.sum(plan.rev) - R.sum(plan.cogs)))}</b></div>
          </div>
          {used > 0 && (
            <div className="banner yellow" style={{ margin: 0, boxShadow: 'none' }}><Icon name="alert" />
              <div>แหล่งงบนี้มีประมาณการแบบจำนวนเครื่อง {used} รายการ — เมื่อใช้ยอดรายเดือน รายการเหล่านั้นไม่ถูกนับ (เก็บไว้ไม่ลบ เปลี่ยนกลับได้)</div></div>
          )}
        </>
      )}

      <label className="field">เป้ายอดขาย (บาท ไม่รวม VAT · ไม่บังคับ)
        <NumInput value={f.target} onValue={(x) => setF({ ...f, target: x })} placeholder="ไม่ตั้งเป้า" ariaLabel="เป้ายอดขาย" />
      </label>
      <label className="field">หมายเหตุ
        <input type="text" value={f.note} maxLength={300} placeholder="เช่น ตามระเบียบต้องจบภายใน ก.ย." onChange={(e) => setF({ ...f, note: e.target.value })} />
      </label>
    </Modal>
  );
}

/** ตารางของ 1 แหล่งงบ: ทุกรุ่น ใส่จำนวน (ราคาต่อตัวตั้งต้นจากรุ่น แก้ได้) ระบบคูณมูลค่าให้ · บันทึกเมื่อออกจากช่อง */
function SalesGrid({ data, source, canEdit, onData }) {
  const year = data.round.year;
  const yy = String(year).slice(-2);
  const [draft, setDraft] = useState({});        // productId → { qty?, price? } ที่ยังไม่บันทึก
  const [busy, setBusy] = useState({});
  const queue = useRef(Promise.resolve());       // บันทึกทีละแถวตามลำดับ กันข้อมูลที่ตอบกลับสลับลำดับ
  const rowOf = (p) => data.deals.find((d) => d.sourceId === source.id && d.productId === p.id && isGridRow(d));
  const cur = (p) => {
    const g = rowOf(p), dr = draft[p.id] || {};
    return { qty: 'qty' in dr ? dr.qty : g ? g.qty : null, price: 'price' in dr ? dr.price : g ? g.price : null };
  };
  const edit = (p, patch) => setDraft((d) => ({ ...d, [p.id]: { ...(d[p.id] || {}), ...patch } }));
  const drop = (id) => setDraft((d) => { const n = { ...d }; delete n[id]; return n; });
  const save = (p) => {
    if (!draft[p.id]) return;
    const g = rowOf(p), c = cur(p);
    const qty = c.qty || 0;
    if (!qty && !g) return;                                     // ยังไม่มีจำนวน: เก็บราคาที่พิมพ์ไว้รอจนกว่าจะใส่จำนวน
    if (g && qty === g.qty && (c.price == null ? null : c.price) === (g.price == null ? null : g.price)) { drop(p.id); return; }
    setBusy((b) => ({ ...b, [p.id]: true }));
    queue.current = queue.current.then(async () => {
      try { onData(await API.saveGrid({ sourceId: source.id, productId: p.id, qty, price: c.price })); drop(p.id); }
      catch (e) { toast(e.message, 'error'); }
      finally { setBusy((b) => ({ ...b, [p.id]: false })); }
    });
  };
  const rows = data.products.filter((p) => canEdit || cur(p).qty);
  let tQty = 0, tValue = 0;
  return (
    <table className="t rvgrid">
      <thead><tr>
        <th>รุ่น</th><th className="num">ราคา/ตัว</th><th className="num">จำนวน</th><th className="num">มูลค่า</th>
        <th className="num">รับรู้ปี {yy}</th><th className="num">ปีถัดไป</th>
      </tr></thead>
      <tbody>
        {rows.length === 0 && <tr><td colSpan={6} className="mute small">ยังไม่มียอดขายในแหล่งงบนี้</td></tr>}
        {rows.map((p) => {
          const c = cur(p);
          const price = c.price == null ? p.price : c.price;
          const value = R.round2((c.qty || 0) * price);
          tQty += c.qty || 0; tValue = R.round2(tValue + value);
          const g = rowOf(p);
          const sch = g && !draft[p.id] ? R.dealSchedule(g, p, year, source) : null;
          return (
            <tr key={p.id} className={c.qty ? '' : 'zero'}>
              <td><b>{p.code}</b> <span className="mute small">{p.name}</span></td>
              <td className="num">
                {canEdit ? (
                  <span onBlur={() => save(p)}>
                    <NumInput className="amt" value={c.price} placeholder={fmt(p.price)} ariaLabel={'ราคาต่อตัว ' + p.code} onValue={(x) => edit(p, { price: x })} />
                  </span>
                ) : fmt(price)}
              </td>
              <td className="num">
                {canEdit ? (
                  <span onBlur={() => save(p)}>
                    <NumInput className="amt qty" nav={'grid-' + source.id} value={c.qty} placeholder="0" ariaLabel={'จำนวน ' + p.code}
                      onValue={(x) => edit(p, { qty: x == null ? null : Math.round(x) })} />
                  </span>
                ) : fmt(c.qty)}
              </td>
              <td className="num"><b>{value ? fmt(value) : ''}</b></td>
              <td className="num">{busy[p.id] ? <span className="mute small">กำลังบันทึก…</span>
                : sch ? (sch.problem ? <span className="tag yellow" title={sch.problem}>ยังไม่มีงวดงาน</span> : fmt(R.sum(sch.rev))) : ''}</td>
              <td className="num mute">{sch && !sch.problem && sch.after.rev ? fmt(sch.after.rev) : ''}</td>
            </tr>
          );
        })}
      </tbody>
      <tfoot><tr><td>รวม</td><td /><td className="num">{fmt(tQty)}</td><td className="num">{fmt(tValue)}</td><td /><td /></tr></tfoot>
    </table>
  );
}

/** เพิ่ม/แก้รุ่นผลิตภัณฑ์: ราคา ต้นทุน 6 หมวด และงวดงาน (ฝ่ายที่ดูแลต้นทุนโครงการ) */
function ProductEditor({ data, product, onClose, onSaved }) {
  const year = data.round.year;
  const seq = useRef(0);
  const withKey = (s) => ({ key: ++seq.current, ...s });
  const [f, setF] = useState(() => product
    ? { code: product.code, name: product.name, price: product.price, costs: { ...EMPTY_COSTS, ...product.costs }, stages: product.stages.map(withKey) }
    : { code: '', name: '', price: null, costs: { ...EMPTY_COSTS }, stages: [withKey({ name: 'ส่งมอบงาน', pct: 100, days: 90 })] });
  const [busy, setBusy] = useState(false);
  const [previewMonth, setPreviewMonth] = useState(1);
  const [vat, setVat] = useState(rvVatPref.get);
  const [showDocs, setShowDocs] = useState(() => !!(product && product.stages.some((s) => s.docs)));
  const set = (patch) => setF((x) => ({ ...x, ...patch }));
  const setStage = (key, patch) => set({ stages: f.stages.map((s) => (s.key === key ? { ...s, ...patch } : s)) });
  const setDoc = (s, part, v) => setStage(s.key, { docs: { customer: '', consultant: '', contractor: '', ...s.docs, [part]: v } });
  const cost = R.unitCost(f);
  const gop = R.round2((Number(f.price) || 0) - cost);
  // ใส่ต้นทุนของงวดครบทุกงวด = % คิดจากสัดส่วนต้นทุน (แบบชีตขั้นรายได้ของวิศวะ) ไม่ใช้ % ที่พิมพ์
  const autoPct = R.pctFromCosts(f.stages.map((s) => s.cost));
  const stages = autoPct ? f.stages.map((s, i) => ({ ...s, pct: autoPct[i] })) : f.stages;
  const someCost = f.stages.some((s) => Number(s.cost) > 0);
  const pctSum = R.round2(stages.reduce((t, s) => t + (Number(s.pct) || 0), 0));
  const problem = stages.length ? R.stagesProblem(stages) : '';
  const used = product ? data.deals.filter((d) => d.productId === product.id).length : 0;
  const others = data.products.filter((p) => !product || p.id !== product.id).filter((p) => p.stages.length);
  const body = () => ({
    code: f.code, name: f.name, price: f.price, costs: f.costs,
    stages: stages.map((s) => ({ name: s.name, pct: s.pct, days: s.days, cost: Number(s.cost) > 0 ? s.cost : null, docs: s.docs || null })),
  });
  const save = async () => {
    if (problem) { toast(problem, 'error'); return; }
    setBusy(true);
    try { onSaved(await API.saveProduct(product && product.id, body()), product ? 'บันทึกรุ่นแล้ว' : 'เพิ่มรุ่นแล้ว'); }
    catch (e) { toast(e.message, 'error'); } finally { setBusy(false); }
  };
  const del = async () => {
    if (!window.confirm('ลบรุ่น ' + product.code + '?')) return;
    setBusy(true);
    try { onSaved(await API.deleteProduct(product.id), 'ลบรุ่นแล้ว'); } catch (e) { toast(e.message, 'error'); } finally { setBusy(false); }
  };
  let cum = 0;
  return (
    <Modal title={product ? 'รุ่น ' + product.code : 'เพิ่มรุ่นผลิตภัณฑ์'} wide="xxl" onClose={onClose} footer={<>
      {product && <button className="btn danger" style={{ marginRight: 'auto' }} disabled={busy || used > 0} title={used ? 'มีในประมาณการขาย ' + used + ' รายการ' : ''} onClick={del}><Icon name="trash" />ลบรุ่น</button>}
      <button className="btn" onClick={onClose}>ยกเลิก</button>
      <button className="btn primary" disabled={busy} onClick={save}><Icon name="check" />บันทึก</button>
    </>}>
      <VatToggle vat={vat} onChange={setVat} />
      <div className="rvform">
        <label className="field">รหัสรุ่น<input type="text" value={f.code} maxLength={30} placeholder="เช่น PL" onChange={(e) => set({ code: e.target.value })} /></label>
        <label className="field grow2">ชื่อรุ่น<input type="text" value={f.name} maxLength={200} placeholder="เช่น POG L" onChange={(e) => set({ name: e.target.value })} /></label>
        <label className="field">ราคาขายต่อเครื่อง{vat ? ' (รวม VAT)' : ' (ไม่รวม VAT)'}<VatInput vat={vat} value={f.price} onValue={(x) => set({ price: x })} placeholder="0" ariaLabel="ราคาขาย" /></label>
      </div>

      <h3 className="rvh3">ต้นทุนต่อเครื่อง{vat ? ' (กรอกแบบรวม VAT)' : ' (ไม่รวม VAT)'}</h3>
      <div className="rvcosts">
        {R.COST_PARTS.map((k) => (
          <label key={k.key} className="field">{k.label}
            <VatInput vat={vat} value={f.costs[k.key]} onValue={(x) => set({ costs: { ...f.costs, [k.key]: x == null ? 0 : x } })} placeholder="0" ariaLabel={'ต้นทุน' + k.label} />
          </label>
        ))}
        <div className="rvkpi"><span>ต้นทุนรวม (ไม่รวม VAT)</span><b>{fmt(cost)}</b></div>
        <div className="rvkpi"><span>กำไรขั้นต้น (GOP)</span><b className={gop < 0 ? 'neg' : ''}>{fmt(gop)}</b><em>{Number(f.price) > 0 ? ((gop / Number(f.price)) * 100).toFixed(1) + '%' : '–'}</em></div>
      </div>

      <h3 className="rvh3">งวดงาน (รับรู้รายได้และต้นทุนตาม % ความสำเร็จ)</h3>
      <p className="small mute" style={{ margin: 0 }}>แต่ละงวดบอกว่าเป็นกี่ % ของงาน และเสร็จกี่วันนับจากวันลงนามสัญญา (นับสะสม) · % ทุกงวดรวมต้องได้ 100 · วันของงวดสุดท้าย = ระยะเวลาติดตั้งมาตรฐาน
        <br />ใส่ "ต้นทุนงวด" ครบทุกงวด ระบบคิด % ให้จากสัดส่วนต้นทุน (แบบชีตขั้นรายได้ของวิศวะ) · ไม่ใส่ = กรอก % เอง</p>
      <div className="rvstagesed">
        <div className="rse-head"><span>#</span><span>ชื่องวด</span><span className="num">ต้นทุนงวด (ใช้คิด %)</span><span className="num">% ของงาน</span><span className="num">วันหลังลงนาม</span><span className="num">สะสม</span><span /></div>
        {stages.map((s, i) => {
          cum = R.round2(cum + (Number(s.pct) || 0));
          return (
            <div key={s.key} className="rse-row">
              <span className="mute small rse-n">{i + 1}</span>
              <input type="text" className="rse-name" value={s.name} maxLength={200} placeholder={'งวดที่ ' + (i + 1)} aria-label={'ชื่องวดที่ ' + (i + 1)} onChange={(e) => setStage(s.key, { name: e.target.value })} />
              <NumInput value={s.cost == null ? null : s.cost} onValue={(x) => setStage(s.key, { cost: x })} placeholder="ไม่ใส่ก็ได้" ariaLabel={'ต้นทุนงวดที่ ' + (i + 1)} />
              {autoPct ? <span className="num rse-auto" title="คิดจากสัดส่วนต้นทุนของงวด">{s.pct}%</span>
                : <NumInput value={s.pct} onValue={(x) => setStage(s.key, { pct: x })} placeholder="%" ariaLabel={'% งวดที่ ' + (i + 1)} />}
              <NumInput value={s.days} onValue={(x) => setStage(s.key, { days: x == null ? null : Math.round(x) })} placeholder="วัน" ariaLabel={'วันหลังลงนาม งวดที่ ' + (i + 1)} />
              <span className="num small">{cum}%</span>
              <button type="button" className="btn ghost icon-only" title="ลบงวดนี้" aria-label={'ลบงวดที่ ' + (i + 1)} onClick={() => set({ stages: f.stages.filter((x) => x.key !== s.key) })}><Icon name="x" /></button>
              {showDocs && (
                <div className="rse-docs">
                  {[['customer', 'ลูกค้า'], ['consultant', 'ที่ปรึกษา'], ['contractor', 'ผู้รับเหมา']].map(([part, label]) => (
                    <label key={part}><span>{label}</span>
                      <input type="text" value={(s.docs && s.docs[part]) || ''} maxLength={300} placeholder={'เอกสารจาก' + label} aria-label={'เอกสารจาก' + label + ' งวดที่ ' + (i + 1)}
                        onChange={(e) => setDoc(s, part, e.target.value)} />
                    </label>
                  ))}
                </div>
              )}
            </div>
          );
        })}
        <div className="rse-foot">
          <button type="button" className="btn sm" onClick={() => {
            const last = f.stages[f.stages.length - 1];
            set({ stages: [...f.stages, withKey({ name: '', pct: R.round2(Math.max(0, 100 - pctSum)) || null, days: last ? Number(last.days) || 0 : 30 })] });
          }}><Icon name="plus" />เพิ่มงวด</button>
          <label className="small rse-docsw"><input type="checkbox" checked={showDocs} onChange={(e) => setShowDocs(e.target.checked)} /> เอกสารยืนยันงวด (ลูกค้า · ที่ปรึกษา · ผู้รับเหมา)</label>
          {others.length > 0 && (
            <select value="" aria-label="คัดลอกงวดงานจากรุ่นอื่น" onChange={(e) => {
              const p = data.products.find((x) => x.id === Number(e.target.value));
              if (p) set({ stages: p.stages.map((s) => withKey({ ...s })) });
            }}>
              <option value="">คัดลอกงวดงานจากรุ่น…</option>
              {others.map((p) => <option key={p.id} value={p.id}>{p.code} · {p.name} ({p.stages.length} งวด)</option>)}
            </select>
          )}
          <span className={'small ' + (problem ? 'err' : 'mute')}>
            รวม {pctSum}%{stages.length ? ' · ติดตั้ง ' + (Number(stages[stages.length - 1].days) || 0) + ' วัน' : ''}{problem ? ' — ' + problem : ''}
            {autoPct ? ' · % คิดจากต้นทุนงวด' : someCost ? ' · ใส่ต้นทุนให้ครบทุกงวดระบบจึงคิด % ให้ (ตอนนี้ใช้ % ที่กรอก)' : ''}
          </span>
        </div>
      </div>
      {!problem && stages.length > 0 && (
        <div className="rvpreview">
          <div className="small">ตัวอย่าง 1 เครื่อง ถ้าลงนาม {R.SIGN_DAY}{' '}
            <MonthPick value={previewMonth} label="เดือนตัวอย่าง" onChange={setPreviewMonth} /> {year}</div>
          <StagePreview deal={{ qty: 1, price: null, signYear: year, signMonth: previewMonth }} product={{ ...f, stages, price: Number(f.price) || 0 }} year={year} />
        </div>
      )}
    </Modal>
  );
}

/** ตาราง 12 เดือนของแหล่งงบแบบยอดรายเดือน (plan = R.sourceMonthlyPlan): รายได้ ต้นทุนงานก่อสร้าง กำไรขั้นต้น */
function SourceMonths({ plan, year }) {
  const gross = plan.rev.map((v, i) => R.round2(v - plan.cogs[i]));
  const row = (label, list, cls) => (
    <tr className={cls}><td>{label}</td>{list.map((v, i) => <td key={i} className="num">{v ? fmt(v) : '–'}</td>)}<td className="num tot">{fmt(R.sum(list))}</td></tr>
  );
  return (
    <div className="tw">
      <table className="t compact rvmonthly rvsrcm">
        <thead><tr><th>ปี {year}</th>{R.MONTHS_TH.map((m) => <th key={m} className="num">{m}</th>)}<th className="num">รวม</th></tr></thead>
        <tbody>
          {row('รายได้', plan.rev, 'strong')}
          {row('ต้นทุนงานก่อสร้าง', plan.cogs, '')}
          {row('กำไรขั้นต้น', gross, 'mute')}
        </tbody>
      </table>
    </div>
  );
}

/** ตารางรายเดือน: รายได้ ต้นทุนงานก่อสร้าง กำไรขั้นต้น (+ แยกรายรุ่น) */
function RevenueMonthly({ data, plan }) {
  const year = data.round.year;
  const gross = plan.rev.map((v, i) => R.round2(v - plan.cogs[i]));
  const T = { rev: R.sum(plan.rev), cogs: R.sum(plan.cogs) };
  const pct = (g, r) => (r > 0 ? ((g / r) * 100).toFixed(1) + '%' : '–');
  const rows = data.products.map((p) => ({ p, g: plan.byProduct[p.id] })).filter((x) => x.g);
  const srcRows = data.sources.map((x) => ({ x, g: plan.bySource[x.id] })).filter((r) => r.g);
  return (
    <div className="card tw">
      <table className="t rvmonthly">
        <thead>
          <tr><th>ปี {year}</th>{R.MONTHS_TH.map((m) => <th key={m} className="num">{m}</th>)}<th className="num">รวมทั้งปี</th><th className="num" title="งวดที่จะเสร็จหลังสิ้นปี">ปีถัดไป</th></tr>
        </thead>
        <tbody>
          <tr className="strong"><td>รายได้ (รับรู้ตาม % ความสำเร็จ)</td>{plan.rev.map((v, i) => <td key={i} className="num">{v ? fmt(v) : '–'}</td>)}<td className="num tot">{fmt(T.rev)}</td><td className="num mute">{fmt(plan.after.rev)}</td></tr>
          <tr><td>ต้นทุนงานก่อสร้าง</td>{plan.cogs.map((v, i) => <td key={i} className="num">{v ? fmt(v) : '–'}</td>)}<td className="num tot">{fmt(T.cogs)}</td><td className="num mute">{fmt(plan.after.cost)}</td></tr>
          <tr className="strong"><td>กำไรขั้นต้น</td>{gross.map((v, i) => <td key={i} className="num">{v ? fmt(v) : '–'}</td>)}<td className="num tot">{fmt(R.round2(T.rev - T.cogs))}</td><td className="num mute">{fmt(R.round2(plan.after.rev - plan.after.cost))}</td></tr>
          <tr className="mute"><td>% กำไรขั้นต้น</td>{gross.map((v, i) => <td key={i} className="num">{pct(v, plan.rev[i])}</td>)}<td className="num tot">{pct(T.rev - T.cogs, T.rev)}</td><td /></tr>
          {srcRows.length > 0 && <tr className="rvsub"><td colSpan={15}>รายได้ของแหล่งงบแบบยอดรายเดือน</td></tr>}
          {srcRows.map(({ x, g }) => (
            <tr key={'s' + x.id}><td>{x.name} <span className="mute small">· GOP {x.gopPct}%</span></td>
              {g.rev.map((v, i) => <td key={i} className="num">{v ? fmt(v) : ''}</td>)}<td className="num tot">{fmt(R.sum(g.rev))}</td><td /></tr>
          ))}
          {rows.length > 0 && <tr className="rvsub"><td colSpan={15}>รายได้แยกรายรุ่น{srcRows.length ? ' (แบบจำนวนเครื่อง × งวดงาน)' : ''}</td></tr>}
          {rows.map(({ p, g }) => (
            <tr key={p.id}><td><span className="mute small">{p.code}</span> {p.name} <span className="mute small">· {g.qty} เครื่อง</span></td>
              {g.rev.map((v, i) => <td key={i} className="num">{v ? fmt(v) : ''}</td>)}<td className="num tot">{fmt(R.sum(g.rev))}</td><td /></tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

function RevenuePage({ me }) {
  const [data, setData] = useState(null);
  const [err, setErr] = useState('');
  const [tab, setTab] = useState(null);
  const [edit, setEdit] = useState(null);          // { kind: 'deal' | 'source' | 'product' | 'import', item }
  const load = useCallback(() => API.revenue().then((d) => { setData(d); setErr(''); }).catch((e) => setErr(e.message)), []);
  useEffect(() => { load(); }, [load]);
  if (err) return <div className="card pad"><p className="err">{err}</p><p><button className="btn sm" onClick={load}>ลองใหม่</button></p></div>;
  if (!data) return <div className="boot">กำลังโหลด…</div>;

  const year = data.round.year;
  const rights = data.rights;
  const view = tab || (rights.project && !rights.sales ? 'products' : rights.sales && !rights.project ? 'sales' : 'summary');
  const plan = R.revenuePlan(data.products, data.deals, year, data.sources);
  const prodById = new Map(data.products.map((p) => [p.id, p]));
  const srcById = new Map(data.sources.map((x) => [x.id, x]));
  const sched = (d) => (prodById.get(d.productId) ? R.dealSchedule(d, prodById.get(d.productId), year, srcById.get(d.sourceId)) : null);
  const saved = (r, msg) => { setData(r); setEdit(null); if (msg) toast(msg); };
  const T = { rev: R.sum(plan.rev), cogs: R.sum(plan.cogs) };
  const signYearOf = (d) => (d.signMonth != null ? d.signYear : (srcById.get(d.sourceId) || {}).signYear || year);
  // รายการขายในแหล่งงบแบบยอดรายเดือนไม่นับ (R.revenuePlan ข้ามให้แล้ว) — มูลค่าขายก็ไม่นับเหมือนกัน
  const counted = data.deals.filter((d) => !plan.bySource[d.sourceId]);
  const thisYear = counted.filter((d) => signYearOf(d) === year);
  const carried = counted.filter((d) => signYearOf(d) < year);
  const valueOf = (list) => R.sum(list.map((d) => { const s = sched(d); return s ? s.value : 0; }));
  const problemProducts = Array.from(new Set(plan.problems.map((x) => x.productId))).map((id) => prodById.get(id)).filter(Boolean);
  const monthlySrc = data.sources.filter((x) => plan.bySource[x.id]);
  const monthlyRev = R.sum(monthlySrc.map((x) => R.sum(plan.bySource[x.id].rev)));
  const noStages = data.products.filter((p) => R.stagesProblem(p.stages));

  // ── ประมาณการขาย: 1 การ์ดต่อแหล่งงบ (ตารางรายรุ่น + รายโครงการที่ระบุเดือนเอง · แบบยอดรายเดือน = ตาราง 12 เดือน) ──
  const groups = [...data.sources.map((x) => ({ x, deals: data.deals.filter((d) => d.sourceId === x.id) })),
    { x: null, deals: data.deals.filter((d) => d.sourceId == null) }].filter((g) => g.x || g.deals.length);
  const sortDeals = (list) => list.slice().sort((a, b) => (a.signYear - b.signYear) || (a.signMonth - b.signMonth) || (a.id - b.id));
  const nRows = counted.filter((d) => d.qty > 0).length + monthlySrc.length;
  const sentence = (d) => { const p = prodById.get(d.productId); return p ? p.code + ' · ' + p.name : '?'; };

  return (
    <div>
      <div className="head">
        <div className="grow">
          <h1>รายได้และต้นทุนงานโครงการ ปี {year}</h1>
          <div className="mute small">รับรู้รายได้และต้นทุนตาม % ความสำเร็จของงาน: งวดงานของแต่ละรุ่น × เดือนที่ลงนาม หรือยอดรายเดือนของแหล่งงบ · ราคาและต้นทุนไม่รวม VAT · ตัวเลขชุดนี้เข้าหน้างบกำไรขาดทุนอัตโนมัติ</div>
        </div>
        {(rights.project || rights.sales) && (
          <button className="btn" onClick={() => setEdit({ kind: 'import' })}><Icon name="upload" />นำเข้าจากไฟล์ Excel</button>
        )}
      </div>

      <div className="tiles">
        <div className="tile t-request"><div className="k">มูลค่าขายรวม (แบบจำนวนเครื่อง)</div><div className="v">{fmt(plan.value)}</div>
          <div className="s">ลงนามปี {year} {fmt(valueOf(thisYear))} · งานยกมา {fmt(valueOf(carried))}</div></div>
        <div className="tile t-budget"><div className="k">รายได้ที่รับรู้ปี {year}</div><div className="v">{fmt(T.rev)}</div>
          <div className="s">{monthlySrc.length ? 'แหล่งงบแบบยอดรายเดือน ' + fmt(monthlyRev) + ' · ' : ''}รอรับรู้ปีถัดไป {fmt(plan.after.rev)}</div></div>
        <div className="tile t-actual"><div className="k">ต้นทุนงานก่อสร้าง ปี {year}</div><div className="v">{fmt(T.cogs)}</div>
          <div className="s">{monthlySrc.length ? 'รับรู้ % เดียวกับรายได้ · แบบยอดรายเดือนใช้ GOP% ของแหล่งงบ' : 'รับรู้ % เดียวกับรายได้'}</div></div>
        <div className="tile t-appr"><div className="k">กำไรขั้นต้น</div><div className="v">{fmt(R.round2(T.rev - T.cogs))}</div>
          <div className="s">{T.rev > 0 ? (((T.rev - T.cogs) / T.rev) * 100).toFixed(1) + '% ของรายได้ (ถ่วงตามที่ขายจริง)' : '–'}</div></div>
      </div>

      {problemProducts.length > 0 && (
        <div className="banner yellow">
          <Icon name="alert" />
          <div><b>ยังคำนวณรายได้ไม่ได้ {plan.problems.length} รายการขาย</b> เพราะรุ่น {problemProducts.map((p) => p.code).join(', ')} ยังไม่ได้ตั้งงวดงานให้ครบ 100%
            {rights.project && <> — ตั้งได้ที่แท็บ "ผลิตภัณฑ์และงวดงาน"</>}</div>
        </div>
      )}

      <div className="rvtabs">
        <div className="seg" role="tablist" aria-label="ส่วนของหน้า">
          <button role="tab" aria-selected={view === 'sales'} className={view === 'sales' ? 'on' : ''} onClick={() => setTab('sales')}>ประมาณการขาย ({nRows})</button>
          <button role="tab" aria-selected={view === 'products'} className={view === 'products' ? 'on' : ''} onClick={() => setTab('products')}>ผลิตภัณฑ์และงวดงาน ({data.products.length})</button>
          <button role="tab" aria-selected={view === 'summary'} className={view === 'summary' ? 'on' : ''} onClick={() => setTab('summary')}>สรุปรายเดือน</button>
        </div>
        <span className="small mute">
          {rights.sales && rights.project ? 'แก้ได้ทุกส่วน' : rights.sales ? 'คุณแก้ประมาณการขายได้' : rights.project ? 'คุณแก้รุ่น ต้นทุน และงวดงานได้' : 'ดูได้อย่างเดียว'}
          {!data.round.salesDept || !data.round.projectDept ? ' · ยังไม่ได้กำหนดฝ่ายผู้กรอกที่หน้าตั้งค่า' : ''}
        </span>
      </div>

      {view === 'sales' && (
        <div>
          {data.products.length === 0 && (
            <div className="banner"><Icon name="alert" /><div>ยังไม่มีรุ่นผลิตภัณฑ์ — ฝ่ายที่ดูแลต้นทุนโครงการ{data.round.projectDept ? ' (' + data.round.projectDept + ')' : ''} ต้องใส่รุ่น ราคา และงวดงานก่อน</div></div>
          )}
          {rights.sales && (
            <div className="rvbar">
              <button className="btn primary sm" onClick={() => setEdit({ kind: 'source', item: null })}><Icon name="plus" />เพิ่มแหล่งงบ</button>
              <button className="btn sm" disabled={!data.products.length} onClick={() => setEdit({ kind: 'deal', item: null })}><Icon name="plus" />เพิ่มรายโครงการ (ระบุเดือนลงนามเอง)</button>
              <span className="small mute">แต่ละแหล่งงบใส่จำนวนของแต่ละรุ่น ระบบคูณราคาให้ · Enter = ไปรุ่นถัดไป บันทึกเองเมื่อออกจากช่อง · งานยกมา: สร้างแหล่งงบที่ลงนามปีก่อน</span>
            </div>
          )}
          {groups.length === 0 && <div className="card pad mute">ยังไม่มีประมาณการขาย{rights.sales ? ' — เริ่มจากเพิ่มแหล่งงบ แล้วใส่จำนวนของแต่ละรุ่น' : ''}</div>}
          {groups.map((g) => {
            const m = g.x && plan.bySource[g.x.id];
            const v = m ? R.sum(m.rev) : valueOf(g.deals);
            const tg = g.x && g.x.target;
            return (
              <div className="card rvgroup" key={g.x ? g.x.id : 'none'}>
                <div className="rvghead">
                  <div className="grow">
                    <b>{g.x ? g.x.name : 'ไม่ระบุแหล่งงบ'}</b>
                    {m ? <span className="small"> · ยอดรายได้รายเดือน · GOP {g.x.gopPct}%{g.x.signYear != null && g.x.signYear < year ? ' · งานยกมา ปี ' + g.x.signYear : ''}</span>
                      : g.x && <span className="small"> · ลงนาม {signWindow(g.x, year)}</span>}
                    {g.x && g.x.note && <span className="small mute"> · {g.x.note}</span>}
                  </div>
                  <div className="rvtarget">
                    <span className="small mute">{m ? 'รายได้ปี ' + String(year).slice(-2) : 'ขาย'} {fmt(v)}{tg ? ' จากเป้า ' + fmt(tg) : ''}</span>
                    {tg > 0 && <span className="bar"><i style={{ width: Math.min(100, (v / tg) * 100) + '%' }} /></span>}
                    {tg > 0 && <b>{Math.round((v / tg) * 100)}%</b>}
                  </div>
                  {rights.sales && g.x && <button className="btn ghost sm" onClick={() => setEdit({ kind: 'source', item: g.x })}><Icon name="edit" />แก้</button>}
                </div>
                {m && <SourceMonths plan={m} year={year} />}
                {m && g.deals.length > 0 && (
                  <p className="small rv-warn rvskip">มีประมาณการแบบจำนวนเครื่อง {g.deals.length} รายการในแหล่งงบนี้ — ไม่นับ เพราะแหล่งงบนี้ใช้ยอดรายเดือน (เปลี่ยนวิธีคิดได้ที่ปุ่ม "แก้")</p>
                )}
                {g.x && !m && <SalesGrid data={data} source={g.x} canEdit={rights.sales} onData={(r) => setData(r)} />}
                {!m && g.deals.some((d) => !isGridRow(d)) && (
                  <table className="t rvdeals">
                    <thead><tr>
                      <th>รายโครงการ (ระบุเดือนเอง)</th><th>รุ่น</th><th className="num">จำนวน</th><th className="num">ราคา/เครื่อง</th><th className="num">มูลค่า</th>
                      <th>ลงนาม</th><th className="num">รับรู้ปี {String(year).slice(-2)}</th><th className="num">ปีถัดไป</th>
                    </tr></thead>
                    <tbody>
                      {sortDeals(g.deals.filter((d) => !isGridRow(d))).map((d) => {
                        const s = sched(d);
                        const p = prodById.get(d.productId);
                        return (
                          <tr key={d.id} className={rights.sales ? 'click' : ''} onClick={() => rights.sales && setEdit({ kind: 'deal', item: d })}>
                            <td>{d.customer || <span className="mute">–</span>}{d.note && <div className="small mute">{d.note}</div>}</td>
                            <td>{sentence(d)}</td>
                            <td className="num">{fmt(d.qty)}</td>
                            <td className="num">{fmt(d.price == null ? (p ? p.price : 0) : d.price)}{d.price != null && p && d.price !== p.price && <div className="small mute">ราคารุ่น {fmt(p.price)}</div>}</td>
                            <td className="num"><b>{s ? fmt(s.value) : '–'}</b></td>
                            <td>{monthYear(d.signYear, d.signMonth)}{d.signYear < year && <span className="tag">ยกมา</span>}</td>
                            <td className="num">{s && s.problem ? <span className="tag yellow" title={s.problem}>ยังไม่มีงวดงาน</span> : s ? fmt(R.sum(s.rev)) : '–'}</td>
                            <td className="num mute">{s && !s.problem && s.after.rev ? fmt(s.after.rev) : ''}</td>
                          </tr>
                        );
                      })}
                    </tbody>
                  </table>
                )}
              </div>
            );
          })}
        </div>
      )}

      {view === 'products' && (
        <div className="card">
          {rights.project && (
            <div className="toolbar">
              <button className="btn primary sm" onClick={() => setEdit({ kind: 'product', item: null })}><Icon name="plus" />เพิ่มรุ่น</button>
              <span className="small mute">ราคาและต้นทุนแสดงแบบไม่รวม VAT · คลิกรุ่นเพื่อแก้ต้นทุนและงวดงาน · นำเข้าทั้งชุดจากไฟล์ของวิศวะได้ที่ปุ่ม "นำเข้าจากไฟล์ Excel" ด้านบน</span>
            </div>
          )}
          {noStages.length > 0 && data.products.length > 0 && (
            <div className="banner yellow rvnostage"><Icon name="alert" />
              <div><b>{noStages.length} รุ่นยังไม่มีงวดงาน</b>: {noStages.map((p) => p.code).join(' · ')}
                <div className="small">ประมาณการขายแบบจำนวนเครื่องของรุ่นเหล่านี้ยังคำนวณรายได้ไม่ได้จนกว่าจะตั้งงวดงาน · แหล่งงบแบบยอดรายเดือนไม่ต้องใช้งวดงาน</div></div>
            </div>
          )}
          {data.products.length === 0 ? <div className="empty">ยังไม่มีรุ่นผลิตภัณฑ์{rights.project ? ' — เพิ่มทีละรุ่น หรือนำเข้าจากไฟล์ Excel' : ''}</div> : (
            <div className="tw">
              <table className="t rvproducts">
                <thead><tr>
                  <th>รหัส</th><th>รุ่น</th><th className="num">ราคาขาย</th><th className="num">ต้นทุน/เครื่อง</th><th className="num">GOP/เครื่อง</th><th className="num">GOP%</th>
                  <th>งวดงาน</th><th className="num">ขายในแผน</th>
                </tr></thead>
                <tbody>
                  {data.products.map((p) => {
                    const c = R.unitCost(p), gp = R.round2(p.price - c), prob = R.stagesProblem(p.stages);
                    const qty = R.sum(data.deals.filter((d) => d.productId === p.id).map((d) => d.qty));
                    return (
                      <tr key={p.id} className={rights.project ? 'click' : ''} onClick={() => rights.project && setEdit({ kind: 'product', item: p })}>
                        <td><b>{p.code}</b></td><td>{p.name}</td>
                        <td className="num">{fmt(p.price)}</td><td className="num">{fmt(c)}</td>
                        <td className={'num' + (gp < 0 ? ' red' : '')}>{fmt(gp)}</td>
                        <td className="num">{p.price > 0 ? ((gp / p.price) * 100).toFixed(1) + '%' : '–'}</td>
                        <td>{prob ? <span className="tag yellow">{prob}</span>
                          : <span className="small">{p.stages.length} งวด · {p.stages[p.stages.length - 1].days} วัน <span className="mute">({p.stages.map((s) => s.pct + '%').join(' · ')})</span></span>}</td>
                        <td className="num">{qty ? fmt(qty) + ' เครื่อง' : <span className="mute">–</span>}</td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          )}
        </div>
      )}

      {view === 'summary' && <RevenueMonthly data={data} plan={plan} />}

      {edit && edit.kind === 'deal' && <DealEditor data={data} deal={edit.item} onClose={() => setEdit(null)} onSaved={saved} />}
      {edit && edit.kind === 'source' && <SourceEditor data={data} source={edit.item} onClose={() => setEdit(null)} onSaved={saved} />}
      {edit && edit.kind === 'product' && <ProductEditor data={data} product={edit.item} onClose={() => setEdit(null)} onSaved={saved} />}
      {edit && edit.kind === 'import' && <RevenueImport data={data} onClose={() => setEdit(null)} onSaved={saved} />}
    </div>
  );
}
