/* entry.jsx — หน้ากรอกงบแบบหน้าเดียวของหน่วยงานต้นสังกัด (ประกอบใน BudgetForm ของ form.jsx)
 *   แถบความคืบหน้า: 1 กิจกรรม · 2 ค่าใช้จ่ายประจำ · 3 ตรวจแล้วส่งงบ
 *   ส่วนที่ 1 กิจกรรม (activity.jsx)
 *   ส่วนที่ 2 ค่าใช้จ่ายประจำ: 1 แถวต่อรหัส ใส่ยอดทั้งปี — เก็บเป็นรายการในกิจกรรมประเภทค่าใช้จ่ายประจำ (PUT /recurring/:acct)
 *   ส่วนเตือน: รหัสแบบกิจกรรมที่ปีก่อนมีงบแต่ปีนี้ยังไม่มีกิจกรรมใช้ → ใส่ในกิจกรรม หรือ ปีหน้าไม่ใช้
 *   หน้าต่าง "ตรวจแล้วส่งงบ": ทุกเรื่องที่ยังค้าง (กติกาเดียวกับที่เซิร์ฟเวอร์ตรวจตอนส่ง) จัดการได้ในหน้าต่างเดียว แล้วส่ง
 * รหัสไหนเป็นแบบกิจกรรม ค่าใช้จ่ายประจำ หรือได้ทั้ง 2 แบบ มาจาก R.entryKindOf — ผู้ดูแลตั้งได้ที่หน้าตั้งค่า
 * "...เท่านั้น" ที่ผู้ดูแลตั้งเองถูกบังคับ (R.entryAllows): กิจกรรมเท่านั้นเพิ่มในตารางนี้ไม่ได้ · ค่าใช้จ่ายประจำเท่านั้นเลือกในกิจกรรมไม่ได้
 * ผู้ใช้ฝ่าย (data.refHidden) ไม่เห็นตัวเลขปีก่อนของเรื่องแบบกิจกรรม (บรรทัดที่มี l.refHidden): ไว้ช่วยนึก / หน้าต่างส่งงบ แสดงแค่รหัสและชื่อ
 * ยกเว้นรหัสค่าใช้จ่ายประจำ — server ส่ง Budget/Actual มาให้ ตารางค่าใช้จ่ายประจำจึงแสดงทุกแถวที่มีตัวเลข (ตัดสินรายแถว ไม่ใช่รายผู้ใช้) */

/**
 * สถานะของหน้ากรอกที่ทุกส่วนใช้ร่วมกัน (คำนวณจากบรรทัดล่าสุดบนหน้าจอ รวมที่ยังรอบันทึก)
 * extra = รหัสที่ผู้ใช้เพิ่มเป็นแถวค่าใช้จ่ายประจำเอง (ยังไม่มียอด)
 */
function entryState(data, lines, extra) {
  const cfg = data.round;
  const acctOf = new Map(data.accounts.map((a) => [a.code, a]));
  const kindOf = (acct) => R.entryKindOf(acctOf.get(acct) || { name: '' });
  const lineOf = new Map(lines.map((l) => [l.acct, l]));
  const inRec = R.monthsByAcct(data.activities.filter((a) => a.kind === 'recurring'));
  // แถวค่าใช้จ่ายประจำ = รหัสแบบค่าใช้จ่ายประจำที่ฝ่ายมีปีนี้ + รหัสที่ใส่ไว้ในค่าใช้จ่ายประจำแล้ว + ที่ผู้ใช้เพิ่มเอง
  const codes = new Set(Object.keys(inRec));
  for (const l of lines) if (l.filler !== 'central' && !l.added && R.entryInRecurring(kindOf(l.acct))) codes.add(l.acct);
  for (const c of extra || []) codes.add(c);
  const rec = Array.from(codes).sort().map((acct) => {
    const l = lineOf.get(acct) || null;
    const months = inRec[acct] || null;
    const amount = months ? R.sum(months) : 0;
    const ev = l ? R.evaluateLine(l, cfg) : null;
    return {
      acct, l, ev, months, amount, kind: kindOf(acct),
      // ตั้งเป็น "กิจกรรมเท่านั้น" แต่มียอดค้างในตารางนี้ (ใส่ไว้ก่อนผู้ดูแลตั้ง) → ล้างได้อย่างเดียว แล้วไปใส่ในกิจกรรม
      locked: !R.entryAllows(acctOf.get(acct) || {}, 'recurring'),
      name: (acctOf.get(acct) || {}).name || (l ? l.name : ''),
      even: !months || amount === 0 || R.patternOf(months).kind === 'even',   // เฉลี่ยทุกเดือน = แก้ยอดทั้งปีในช่องได้
      other: ev && ev.filled && !l.declined ? R.round2(ev.R - amount) : 0,     // ยอดของรหัสนี้ที่อยู่ในกิจกรรม
    };
  });
  const own = lines.filter((l) => l.filler !== 'central').map((l) => ({ l, ev: R.evaluateLine(l, cfg) }));
  const actKind = (v) => !R.deptFillsLine(kindOf(v.l.acct)) && !codes.has(v.l.acct);
  const acts = data.activities.filter((a) => a.kind === 'activity');
  const need = rec.filter((r) => r.l);
  const items = acts.flatMap((a) => a.items);
  const st = {
    rec, kindOf,
    // ไว้ช่วยนึก (ไม่บังคับ): ปีก่อนมีงบเรื่องนี้ แต่ปีนี้ยังไม่มีรายการไหนที่ฝ่ายบัญชีจัดเข้ารหัสนี้ (ผู้ใช้ฝ่ายรู้แค่ว่ามีงบ ไม่เห็นยอด)
    hints: own.filter((v) => actKind(v) && v.ev.hasBudget && !(v.ev.R > 0)),
    recTodo: need.filter((r) => !r.ev.filled),
    empty: data.activities.filter((a) => !a.items.length),
    // กิจกรรมที่มีรายการยังไม่ใส่หมายเหตุ (บันทึกไว้ก่อนมีกติกาบังคับ) — กติกาเดียวกับ server ตอนส่งงบ
    itemNotes: R.itemNotesMissing(acts),
    // คำชี้แจงที่ต้องใส่ก่อนส่ง: เฉพาะบรรทัดที่ฝ่ายใส่ยอดเอง (กติกาเดียวกับ server ตอนส่งงบ)
    notes: own.filter((v) => R.deptFillsLine(kindOf(v.l.acct)) && v.ev.noteMissing),
    nAct: acts.length, actAmt: R.sum(acts.map(R.activityTotal)),
    recAmt: R.sum(rec.map((r) => r.amount)), recNeed: need.length, recDone: need.filter((r) => r.ev.filled).length,
    // รายการในกิจกรรมที่ฝ่ายบัญชีจัดเข้ารหัสแล้ว / ทั้งหมด · ยอดที่ยังไม่จัด (รวมในยอดขอ แต่ยังไม่อยู่ในบรรทัดรหัสบัญชี)
    unmapped: R.unmappedOf(acts), nItems: items.length, nMapped: items.filter((it) => R.mapState(it) === 'done').length,
  };
  st.issues = st.empty.length + st.itemNotes.length + st.recTodo.length + st.notes.length;
  return st;
}

/**
 * แถบสรุปบนหัวหน้ากรอก: ยอดขอ · Budget ปีอ้างอิง · เพิ่ม/ลดเทียบ Budget · ความคืบหน้า
 * done/total = รายการที่ผู้ใช้คนนี้รับผิดชอบ (ตัวเดียวกับตัวกรอง ครบแล้ว/ยังไม่ครบ)
 * ยังระบุไม่ครบทุกรหัส = เทียบ Budget เฉพาะรหัสที่ระบุแล้ว (เทียบทั้งฝ่ายตอนยังใส่ไม่ครบจะได้ "ลด 97%" ที่ไม่มีความหมาย)
 */
function DeptSummary({ data, lines, done, total }) {
  const cfg = data.round;
  const L = labelsOf(cfg);
  const sum = summarize(lines, cfg);
  // รายการในกิจกรรมที่ฝ่ายบัญชียังไม่จัดเข้ารหัส: เป็นยอดขอของฝ่ายแล้ว แต่ยังไม่อยู่ในบรรทัดรหัสบัญชีไหน
  const U = R.unmappedOf(data.activities).amount;
  const complete = sum.n > 0 && sum.filled === sum.n;
  const cmp = complete ? sum : summarize(lines.filter((l) => R.evaluateLine(l, cfg).filled), cfg);
  const cmpR = R.round2(cmp.R + U);
  const diff = R.round2(cmpR - cmp.B);
  const pct = cmp.B > 0 ? (diff / cmp.B) * 100 : null;
  const over = pct != null && pct > Number(cfg.redPct) && diff > Number(cfg.redMinBaht || 0);
  const tone = !cmp.filled && !U ? 'pending' : over ? 'over' : diff > 0 ? 'up' : 'down';
  const pctDone = total ? Math.floor((done / total) * 100) : 0;
  const requestTile = (
    <div className="tile t-request">
      <div className="k">{L.request}</div>
      <div className="v">{fmt(R.round2(sum.R + U))}</div>
      <div className="s">{U > 0 ? 'รวมรายการที่รอฝ่ายบัญชีจัดรหัส ' + fmt(U) + ' บาท' : complete ? 'ระบุครบแล้ว' : 'ยังมีค่าใช้จ่ายประจำที่ยังไม่ใส่ยอด'}</div>
    </div>
  );
  const statusTile = (
    <div className={'tile t-status' + (pctDone >= 100 ? ' full' : '')}>
      <div className="k">ความคืบหน้า</div>
      <div className="v">{total ? pctDone + '%' : '–'}</div>
      <div className="prog" title={'ครบแล้ว ' + done + ' จาก ' + total + ' รายการ'}><span className="bar"><i style={{ width: pctDone + '%' }} /></span></div>
      <div className="s">ครบแล้ว {done}/{total} รายการ</div>
    </div>
  );
  // ผู้ใช้ฝ่ายไม่เห็นตัวเลขปีก่อน: แสดงยอดขอแยกตามที่มา (กิจกรรม · ค่าใช้จ่ายประจำ) แทน Budget และส่วนต่าง
  if (data.refHidden) {
    const acts = data.activities.filter((a) => a.kind === 'activity');
    const recs = data.activities.filter((a) => a.kind === 'recurring');
    const nRec = new Set(recs.flatMap((a) => a.items.map((it) => it.acct))).size;
    return (
      <div className="tiles dsum">
        {requestTile}
        <div className="tile t-act">
          <div className="k">กิจกรรม</div>
          <div className="v">{fmt(R.sum(acts.map(R.activityTotal)))}</div>
          <div className="s">{acts.length} กิจกรรม · {R.sum(acts.map((a) => a.items.length))} รายการ</div>
        </div>
        <div className="tile t-rec">
          <div className="k">ค่าใช้จ่ายประจำ</div>
          <div className="v">{fmt(R.sum(recs.map(R.activityTotal)))}</div>
          <div className="s">{nRec} รายการ</div>
        </div>
        {statusTile}
      </div>
    );
  }
  return (
    <div className="tiles dsum">
      {requestTile}
      <div className="tile t-budget">
        <div className="k">{L.budget}</div>
        <div className="v">{fmt(sum.B)}</div>
        <div className="s">{L.actual} {cfg.actualMonths ? fmt(sum.A) : '–'}</div>
      </div>
      <div className={'tile t-diff ' + tone}
        title={complete ? '' : 'เทียบเฉพาะที่ระบุแล้ว: ยอดขอ ' + fmt(cmpR) + ' · ' + L.budget + ' ' + fmt(cmp.B)}>
        <div className="k">เทียบ {L.budget}{complete ? '' : ' (เฉพาะที่ระบุแล้ว)'}</div>
        <div className="v">{(!cmp.filled && !U) || pct == null ? '–' : fmtPct(pct)}</div>
        <div className="s">
          {!cmp.filled && !U ? 'ยังไม่มียอดขอ' : (diff > 0 ? 'เพิ่ม ' : diff < 0 ? 'ลด ' : 'เท่ากัน ') + (diff ? fmt(Math.abs(diff)) + ' บาท' : '')}
          {sum.red > 0 ? ' · เกินเกณฑ์ ' + sum.red + ' รายการ' : ''}
        </div>
      </div>
      {statusTile}
    </div>
  );
}

/** หัวข้อของแต่ละส่วนบนหน้ากรอก (บอกว่ากรอบข้างล่างคืออะไร) */
function FormSecTitle({ title, hint }) {
  return <div className="fsec-h"><b>{title}</b>{hint && <span>{hint}</span>}</div>;
}

/** สลับมุมมองของหน้าฝ่าย: รายงาน (เปิดมาเจอก่อน) · กรอกงบ · สรุปตามรหัสบัญชี */
const FORM_VIEWS = [
  { key: 'report', label: 'รายงาน', hint: 'ภาพรวมคำขอของฝ่าย อ่านอย่างเดียว · จะเพิ่มหรือแก้ยอด กด "กรอกงบ"' },
  { key: 'entry', label: 'กรอกงบ', hint: 'ใส่กิจกรรมและค่าใช้จ่ายประจำ แล้วตรวจและส่งงบ' },
  { key: 'sum', label: 'สรุปตามรหัสบัญชี', hint: 'ยอดรวมของฝ่ายแยกตามรหัสบัญชี ทุกรหัส' },
];
function FormViews({ view, setView }) {
  const cur = FORM_VIEWS.find((v) => v.key === view) || FORM_VIEWS[0];
  return (
    <div className="fviews">
      <div className="seg lg" role="tablist" aria-label="มุมมองของหน้านี้">
        {FORM_VIEWS.map((v) => (
          <button key={v.key} role="tab" aria-selected={view === v.key} className={view === v.key ? 'on' : ''} onClick={() => setView(v.key)}>{v.label}</button>
        ))}
      </div>
      <span className="small mute">{cur.hint}</span>
    </div>
  );
}

/** แถบ "ขั้นตอนการกรอกงบ" 1 กิจกรรม → 2 ค่าใช้จ่ายประจำ → 3 ตรวจแล้วส่งงบ (กดแต่ละขั้นเพื่อไปยังส่วนนั้น)
 * แต่ละขั้นบอกสถานะด้วยสี: ok = เสร็จ (เขียว) · cur = กำลังทำ (น้ำเงิน) · warn = มีเรื่องค้าง (ส้ม) */
function EntrySteps({ st, data, onStep, onReview }) {
  const sent = data.status === 'submitted' || data.status === 'locked';
  const s1 = st.empty.length || st.itemNotes.length ? 'warn' : st.nAct > 0 ? 'ok' : 'cur';
  const s2 = st.recNeed === 0 || st.recDone === st.recNeed ? 'ok' : 'cur';
  const recPct = st.recNeed ? Math.floor((st.recDone / st.recNeed) * 100) : 100;
  const mark = (state, no) => (state === 'ok' ? <Icon name="check" /> : no);
  return (
    <div className="esteps">
      <button className={'step ' + s1} onClick={() => onStep('sec-act')}>
        <b>{mark(s1, '1')}</b>
        <span className="sbody">
          <span className="st">กิจกรรม</span>
          <span className="ss">{st.nAct} กิจกรรม · {fmt(st.actAmt)} บาท</span>
          {st.empty.length > 0 ? <span className="sn">มีกิจกรรมที่ยังไม่มีรายการ {st.empty.length}</span>
            : st.itemNotes.length > 0 ? <span className="sn">มีกิจกรรมที่ยังไม่ใส่หมายเหตุ {st.itemNotes.length}</span>
            : st.nItems > 0 ? <span className="sm" title="ฝ่ายบัญชีจัดรายการในกิจกรรมเข้ารหัสบัญชีให้ ส่งงบได้โดยไม่ต้องรอ">ฝ่ายบัญชีจัดรหัสแล้ว {st.nMapped}/{st.nItems} รายการ</span> : null}
        </span>
      </button>
      <button className={'step ' + s2} onClick={() => onStep('sec-rec')}>
        <b>{mark(s2, '2')}</b>
        <span className="sbody">
          <span className="st">ค่าใช้จ่ายประจำ</span>
          <span className="ss">{st.recNeed ? 'ใส่แล้ว ' + st.recDone + '/' + st.recNeed + ' รายการ' : 'ไม่มีรายการที่ต้องใส่'} · {fmt(st.recAmt)} บาท</span>
          {st.recNeed > 0 && <span className="sbar" title={recPct + '%'}><i style={{ width: recPct + '%' }} /></span>}
        </span>
      </button>
      <button className={'step ' + (sent ? 'ok' : st.issues ? 'warn' : 'ok')} onClick={onReview}>
        <b>{!sent && st.issues ? '3' : <Icon name="check" />}</b>
        <span className="sbody"><span className="st">ตรวจแล้วส่งงบ</span>
          <span className="ss">{data.status === 'locked' ? 'ล็อกแล้ว' : data.status === 'submitted' ? 'ส่งแล้ว รอผู้ดูแลตรวจ' : st.issues ? 'เหลือ ' + st.issues + ' เรื่อง' : 'พร้อมส่ง'}</span></span>
      </button>
    </div>
  );
}

/** ยอดรายเดือนของค่าใช้จ่ายประจำ 1 รหัส (เช่น ปรับเงินเดือนตั้งแต่ เม.ย.) */
function RecurringMonths({ r, data, onClose, onSave }) {
  const cfg = data.round;
  const [months, setMonths] = useState(() => (r.months ? r.months.slice() : R.zeros()));
  const ref = r.l && !r.l.added && !r.l.refHidden ? r.l : null;
  return (
    <Modal title={r.name + ' · ยอดรายเดือน'} wide="xl" onClose={onClose} footer={<>
      <span className="small" style={{ marginRight: 'auto', alignSelf: 'center' }}>รวมทั้งปี <b style={{ fontSize: 16 }}>{fmt(R.sum(months))}</b> บาท</span>
      <button className="btn" onClick={onClose}>ยกเลิก</button>
      <button className="btn primary" onClick={() => onSave(months)}><Icon name="check" />บันทึก</button>
    </>}>
      <div className="recur-grid">
        <MonthsEditor months={months} cfg={cfg} nav="recm" budget={ref ? ref.budget : null} actual={ref ? ref.actual : null} onChange={setMonths} />
        <div className="recur-help small">
          <div className="quick">
            {ref && <button type="button" className="btn sm" onClick={() => setMonths(ref.budget.map((x) => Math.max(0, R.round2(x))))}>ตาม Budget {cfg.baseYear} ทุกเดือน</button>}
            <button type="button" className="btn sm" onClick={() => setMonths(R.spread(R.sum(months), null))}>เฉลี่ยเท่ากันทุกเดือน</button>
          </div>
          <p className="mute">พิมพ์ยอดแต่ละเดือน (Enter = เดือนถัดไป){ref ? ' · ตัวเลขสีจาง = Budget/Actual ปี ' + cfg.baseYear + ' ไว้เทียบ' : ''}</p>
          <p className="mute">ใช้เมื่อแต่ละเดือนไม่เท่ากัน เช่น ปรับเงินเดือนตั้งแต่ เม.ย. หรือจ่ายเป็นงวด</p>
        </div>
      </div>
    </Modal>
  );
}

/** ส่วนที่ 2: ค่าใช้จ่ายประจำ — 1 แถวต่อรหัส ใส่ยอดทั้งปี (เฉลี่ยทุกเดือน) หรือรายเดือน · บันทึกทันทีเมื่อออกจากช่อง */
function RecurringTable({ deptCode, data, lines, canEdit, beforeWrite, onData, actions, showErrors, noteOpen, focus }) {
  const cfg = data.round;
  const L = labelsOf(cfg);
  // ตัวเลขปีก่อนของแถว: ผู้ดูแล/ฝ่ายบัญชีเห็นทุกแถว · ผู้ใช้ฝ่ายเห็นเฉพาะรหัสค่าใช้จ่ายประจำ (server ไม่ส่งตัวเลขของบรรทัดที่มี refHidden)
  const refOf = (r) => !!(r.l && !r.l.refHidden);
  const [extra, setExtra] = useState([]);
  const [draft, setDraft] = useState({});        // รหัส → ยอดที่กำลังพิมพ์ (ยังไม่บันทึก)
  const [busy, setBusy] = useState({});
  const [monthsFor, setMonthsFor] = useState(null);
  const [adding, setAdding] = useState(false);
  const [addQ, setAddQ] = useState('');
  const queue = useRef(Promise.resolve());       // บันทึกทีละรายการตามลำดับ กันข้อมูลที่ตอบกลับมาสลับลำดับกัน
  const rows = useMemo(() => entryState(data, lines, extra).rec, [data, lines, extra]);

  const dropDraft = (acct) => setDraft((d) => { const n = { ...d }; delete n[acct]; return n; });
  const send = (acct, body, msg) => {
    setBusy((b) => ({ ...b, [acct]: true }));
    queue.current = queue.current.then(async () => {
      try {
        await beforeWrite();
        onData(await API.setRecurring(deptCode, acct, body));
        dropDraft(acct);
        if (msg) toast(msg);
      } catch (e) { toast(e.message, 'error'); }
      finally { setBusy((b) => ({ ...b, [acct]: false })); }
    });
    return queue.current;
  };
  const commit = (r) => {
    if (!(r.acct in draft)) return;
    const v = draft[r.acct] == null ? 0 : R.round2(draft[r.acct]);
    if (v === r.amount) { dropDraft(r.acct); return; }
    send(r.acct, v > 0 ? { amount: v } : {});
  };
  const focusRow = (acct) => setTimeout(() => {
    const el = document.getElementById('rec-' + acct);
    if (!el) return;
    el.scrollIntoView({ block: 'center', behavior: 'smooth' });
    const inp = el.querySelector('input.amt');
    if (inp) inp.focus();
  }, 80);
  // มาจากที่อื่น ("ใส่ยอด" ในตารางสรุป / หน้าต่างตรวจก่อนส่ง): เพิ่มแถวถ้ายังไม่มี แล้ววางเคอร์เซอร์ที่ช่องยอด
  useEffect(() => {
    if (!focus) return;
    if (!rows.some((r) => r.acct === focus.acct)) setExtra((x) => [...x, focus.acct]);
    focusRow(focus.acct);
  }, [focus]);

  const total = R.sum(rows.map((r) => r.amount));
  const shownCodes = new Set(rows.map((r) => r.acct));
  const needle = addQ.trim().toLowerCase();
  // รหัสที่ผู้ดูแลตั้งเป็น "กิจกรรมเท่านั้น" ไม่อยู่ในรายการ · ค่าใช้จ่ายประจำ / ได้ทั้ง 2 แบบ ขึ้นก่อน
  const recFirst = (a) => (R.entryInRecurring(R.entryKindOf(a)) ? 0 : 1);
  const addList = !adding ? [] : data.accounts
    .filter((a) => a.filler !== 'central' && R.entryAllows(a, 'recurring') && !shownCodes.has(a.code) && (!needle || a.code.includes(needle) || a.name.toLowerCase().includes(needle)))
    .sort((x, y) => (recFirst(x) - recFirst(y)) || (x.code < y.code ? -1 : 1));
  const monthsRow = monthsFor && rows.find((r) => r.acct === monthsFor);
  const withRef = !data.refHidden || rows.some(refOf);

  return (
    <div className="card" id="sec-rec">
      <div className="sechead">
        <span className="secno">2</span>
        <div className="grow">
          <h2>ค่าใช้จ่ายประจำ</h2>
          <div className="small mute">ค่าใช้จ่ายที่เกิดทุกเดือน ฝ่ายบัญชีกำหนดรายการไว้ให้แล้ว ใส่ยอดทั้งปี ระบบแบ่งเท่ากันทุกเดือน</div>
        </div>
        <div className="sectotal"><span className="small mute">รวม {rows.filter((r) => r.amount > 0).length} รายการ</span><b>{fmt(total)}</b></div>
      </div>
      {/* withref: Budget และ Actual ปีอ้างอิงเป็นคอลัมน์ของตัวเอง เทียบกับยอดขอในแถวเดียวกัน · ปุ่มย้ายไปใต้ชื่อ */}
      <div className={'rectable' + (withRef ? ' withref' : '')}>
        {!withRef ? (
          <div className="recrow head" aria-hidden="true">
            <span>ค่าใช้จ่าย · รหัสบัญชี</span>
            <span className="num">ขอปี {cfg.year}</span><span />
          </div>
        ) : (
          <div className="recrow head" aria-hidden="true">
            <span className="rc-name">ค่าใช้จ่าย</span>
            <span className="num rc-bud">Budget {cfg.baseYear}</span>
            <span className="num rc-actl">Actual {cfg.baseYear}<small>{actualRange(cfg)}</small></span>
            <span className="num rc-amt">ขอปี {cfg.year}</span>
          </div>
        )}
        {rows.length === 0 && <div className="empty">ฝ่ายนี้ไม่มีค่าใช้จ่ายประจำ{canEdit ? ' — เพิ่มได้ที่ปุ่มด้านล่าง' : ''}</div>}
        {rows.map((r) => {
          const ev = r.ev;
          const declined = !!(r.l && r.l.declined);
          const todo = !!(r.l && ev && !ev.filled);
          const base = ev ? (ev.P > 0 ? ev.P : ev.B) : 0;
          const val = r.acct in draft ? draft[r.acct] : r.amount || null;
          const noteShow = canEdit && ev && ev.noteRequired && (ev.noteMissing || noteOpen[r.acct]);
          return (
            <div key={r.acct} id={'rec-' + r.acct} className={'recrow' + (todo ? ' todo' : '') + (declined ? ' declined' : '') + (ev && ev.flag !== 'none' ? ' ' + ev.flag : '')}>
              <span className="rc-name">
                <b>{r.name}</b>
                <span className="small mute">
                  {r.acct}{r.locked ? '' : r.kind === 'activity' ? ' · ปกติใส่ในกิจกรรม' : ''}
                  {r.l && r.l.mergedFrom && r.l.mergedFrom.length > 0 && <MergedNote codes={r.l.mergedFrom} accounts={data.accounts} />}
                  {r.locked && <span className="tag red" title={(acctOfCode(data.accounts, r.acct).mergeInto ? 'ฝ่ายบัญชีรวมรหัสนี้เข้ารหัส ' + acctOfCode(data.accounts, r.acct).mergeInto : 'ฝ่ายบัญชีตั้งรหัสนี้เป็น "กิจกรรมเท่านั้น"') + ' — ล้างยอดในตารางนี้ แล้วใส่ในที่ใหม่แทน'}>
                    {acctOfCode(data.accounts, r.acct).mergeInto ? 'รวมเข้ารหัส ' + acctOfCode(data.accounts, r.acct).mergeInto + ' แล้ว' : 'กิจกรรมเท่านั้น · ย้ายไปใส่ในกิจกรรม'}
                  </span>}
                  {ev && ev.flag !== 'none' && <span className={'tag ' + ev.flag} title={R.flagText(ev, cfg)}>{FLAG_LABEL[ev.flag]}</span>}
                </span>
              </span>
              {withRef && (
                <span className="num rc-bud" title={refOf(r) ? '' : 'ไม่แสดงยอดปีก่อนของค่าใช้จ่ายแบบกิจกรรม'}>
                  <em className="rc-lbl">{L.budget}</em>{refOf(r) && ev.B ? fmt(ev.B) : '–'}
                </span>
              )}
              {withRef && (
                <span className="num rc-actl" title={refOf(r) && ev.P != null ? L.forecast + ' ' + fmt(ev.P) + ' (คิดจาก Actual เฉลี่ยต่อเดือน × 12)' : ''}>
                  <em className="rc-lbl">{L.actual}</em>{refOf(r) && cfg.actualMonths ? fmt(ev.A) : '–'}
                  {refOf(r) && ev.P != null && <small>คาดทั้งปี {fmt(ev.P)}</small>}
                </span>
              )}
              <span className="rc-amt">
                {declined ? <span className="mute small">ปีหน้าไม่ใช้</span>
                  : canEdit && r.even && !r.locked ? (
                    <span onBlur={() => commit(r)}>
                      <NumInput className="amt" nav="rec" value={val} placeholder={todo ? 'ใส่ยอด' : '0'} ariaLabel={'ยอดขอปี ' + cfg.year + ' ' + r.name}
                        onValue={(x) => setDraft((d) => ({ ...d, [r.acct]: x }))} />
                    </span>
                  ) : (
                    <span className="num amt-ro" title={r.even ? '' : 'แต่ละเดือนไม่เท่ากัน แก้ที่ "รายเดือน"'}>{r.amount ? fmt(r.amount) : '–'}</span>
                  )}
                {!r.even && !declined && <small className="mute">แต่ละเดือนไม่เท่ากัน</small>}
                {r.other > 0 && <small className="mute" title="ยอดของค่าใช้จ่ายนี้ที่ใส่ไว้ในกิจกรรม รวมอยู่ในยอดขอของรหัสนี้ด้วย">+ ในกิจกรรม {fmt(r.other)}</small>}
                {busy[r.acct] && <small className="mute">กำลังบันทึก…</small>}
              </span>
              <span className="rc-act">
                {canEdit && r.locked && r.amount > 0 && (
                  <button className="pill" onClick={() => send(r.acct, {}, 'ล้างยอดแล้ว ใส่ค่าใช้จ่ายนี้ในกิจกรรมแทน')} title="ล้างยอดในตารางค่าใช้จ่ายประจำ แล้วไปใส่ในกิจกรรม">ล้างยอด</button>
                )}
                {canEdit && refOf(r) && !declined && !r.locked && base > 0 && (
                  <button className="pill" onClick={() => send(r.acct, { amount: base })} title={'ใส่ ' + fmt(base) + ' เท่า ' + (ev.P > 0 ? L.forecast : L.budget)}>เท่าปีก่อน</button>
                )}
                {canEdit && !declined && !r.locked && <button className="pill" onClick={() => setMonthsFor(r.acct)} title="ใส่ยอดแต่ละเดือนเอง">รายเดือน</button>}
                {canEdit && !declined && !r.locked && r.l && !(r.other > 0) && (
                  <button className="pill" onClick={() => send(r.acct, { declined: true }, R.evaluateLine({ ...r.l, months: R.zeros(), declined: true }, cfg).needReason ? 'เลือก "ปีหน้าไม่ใช้" แล้ว ใส่เหตุผลใต้รายการนี้' : '')} title={'ปี ' + cfg.year + ' ไม่ใช้ค่าใช้จ่ายนี้'}>ไม่ใช้</button>
                )}
                {canEdit && declined && <button className="pill" onClick={() => actions.undecline(r.acct)}>ยกเลิก "ไม่ใช้"</button>}
              </span>
              {noteShow && (
                <div className="lsub">
                  <FlagMsg ev={ev} cfg={cfg} />
                  <NoteBox l={r.l} ev={ev} own src={[]} editing stale={false} showErrors={showErrors} actions={actions} />
                </div>
              )}
              {!noteShow && ev && ev.noteRequired && r.l.note && <div className="rc-note small"><span className="mute">คำชี้แจง: </span>{r.l.note}</div>}
            </div>
          );
        })}
      </div>
      {canEdit && (
        <div className="recfoot">
          {!adding ? (
            <button className="btn sm" onClick={() => setAdding(true)}><Icon name="plus" />เพิ่มค่าใช้จ่ายประจำอื่น</button>
          ) : (
            <div className="recadd">
              <div className="search">
                <Icon name="search" />
                <input type="text" autoFocus value={addQ} onChange={(e) => setAddQ(e.target.value)} placeholder="ค้นชื่อค่าใช้จ่าย เช่น ค่าเช่า" aria-label="ค้นค่าใช้จ่ายที่จะเพิ่ม" />
              </div>
              <button className="btn sm" onClick={() => { setAdding(false); setAddQ(''); }}>ปิด</button>
              <div className="picklist">
                {addList.length === 0 && <div className="empty small">ไม่พบ</div>}
                {addList.slice(0, 60).map((a) => (
                  <button key={a.code} onClick={() => { setExtra((x) => [...x, a.code]); setAdding(false); setAddQ(''); focusRow(a.code); }}>
                    <span className="mute num" style={{ width: 70, textAlign: 'left' }}>{a.code}</span>
                    <span style={{ flex: 1 }}>{a.name}</span>
                    {R.entryKindOf(a) === 'activity' && <span className="mute small">ปกติใส่ในกิจกรรม</span>}
                    {R.entryKindOf(a) === 'both' && <span className="mute small">ได้ทั้ง 2 แบบ</span>}
                  </button>
                ))}
              </div>
            </div>
          )}
          <span className="small mute">พิมพ์ยอดแล้วกด Enter ไปช่องถัดไปได้เลย ระบบบันทึกให้ทุกครั้งที่ออกจากช่อง</span>
        </div>
      )}
      {monthsRow && (
        <RecurringMonths r={monthsRow} data={data} onClose={() => setMonthsFor(null)}
          onSave={(months) => { setMonthsFor(null); send(monthsRow.acct, R.sum(months) > 0 ? { months } : {}); }} />
      )}
    </div>
  );
}

/** ไว้ช่วยนึก (ไม่บังคับ ไม่กันการส่งงบ): เรื่องที่ปีก่อนมีงบ แต่ปีนี้ยังไม่มีรายการไหนที่ฝ่ายบัญชีจัดเข้ามา
 * ฝ่ายไม่ต้องเลือก "ไม่ใช้" แล้ว — ไม่มีรายการ = ไม่ได้ขอ · รายการที่ยังรอฝ่ายบัญชีจัดรหัสอาจเป็นเรื่องเดียวกันก็ได้
 * ผู้ใช้ฝ่ายเห็นแค่รหัสและชื่อค่าใช้จ่าย (ไม่เห็นยอดปีก่อน) · ผู้ดูแล/ฝ่ายบัญชีเห็น Budget และ Actual */
function BudgetHints({ data, st, canEdit, actions }) {
  const cfg = data.round;
  const [open, setOpen] = useState(false);
  if (!st.hints.length) return null;
  const list = st.hints.slice().sort((a, b) => b.ev.B - a.ev.B || (a.l.acct < b.l.acct ? -1 : 1));
  // รายการในกิจกรรมที่ชื่อตรงกับเรื่องนี้แต่ยังรอฝ่ายบัญชีจัดรหัส (เช่น กด "เพิ่มในกิจกรรม" จากการ์ดนี้) — ยังไม่เข้ารหัสจึงยังอยู่ในการ์ด บอกไว้ว่ากรอกแล้ว
  const acts = data.activities.filter((a) => a.kind === 'activity');
  const enteredOf = (name) => {
    const found = acts.map((a) => ({ a, items: a.items.filter((it) => !it.acct && sameName(it.name, name)) })).filter((x) => x.items.length);
    const items = found.flatMap((x) => x.items);
    return { n: items.length, amount: R.sum(items.map(R.itemAmount)), where: found.map((x) => '"' + x.a.name + '"').join(', ') };
  };
  const nEntered = list.filter((v) => enteredOf(v.l.name).n > 0).length;
  return (
    <div className="card hintcard" id="sec-remind">
      <button className="hinthead" onClick={() => setOpen(!open)} aria-expanded={open}>
        <Icon name="note" />
        <span className="grow">
          <b>ไว้ช่วยนึก: ปีก่อนมีงบเรื่องเหล่านี้ ({list.length}{nEntered > 0 ? ' · กรอกแล้ว ' + nEntered : ''})</b>
          <span className="small mute">ปีนี้ยังไม่มีรายการไหนเข้ามา ถ้ายังมีแผน เพิ่มเป็นกิจกรรมได้ · ไม่บังคับ ไม่กันการส่งงบ</span>
        </span>
        <Icon name="chevron" className={'chev' + (open ? ' open' : '')} />
      </button>
      {open && (
        <div className="remlist">
          {list.map((v) => {
            const e = enteredOf(v.l.name);
            return (
            <div key={v.l.acct} className={'remrow' + (e.n > 0 ? ' entered' : '')}>
              <span className="rm-name">
                <b>{v.l.name}</b>
                <span className="small mute">{data.refHidden ? 'รหัส ' + v.l.acct : v.l.acct + ' · ปี ' + cfg.baseYear + ': Budget ' + fmt(v.ev.B) + ' · Actual ' + fmt(v.ev.A)}</span>
                {e.n > 0 && <span className="rm-entered"><Icon name="check" />กรอกแล้ว {e.n} รายการ {fmt(e.amount)} บาท ในกิจกรรม {e.where} · รอฝ่ายบัญชีจัดรหัส</span>}
              </span>
              {canEdit && <button className="btn sm" onClick={() => actions.addToActivity(v.l.acct)}><Icon name="plus" />เพิ่มในกิจกรรม</button>}
            </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

/**
 * หน้าต่าง "ตรวจแล้วส่งงบ": รวมทุกเรื่องที่ยังค้าง (กิจกรรมว่าง · ค่าใช้จ่ายที่ยังไม่ระบุ · คำชี้แจง) จัดการได้ในหน้าต่างนี้
 * แล้วส่ง · รายการที่ต้องชี้แจงจำไว้ตั้งแต่เปิดหน้าต่าง ช่องพิมพ์จึงไม่หายไประหว่างพิมพ์
 */
function SubmitReview({ data, lines, st, canEdit, canSubmit, busy, actions, showErrors, onClose, onSubmit, onEditActivity, onRecurring, onDeclineRec }) {
  const cfg = data.round;
  const L = labelsOf(cfg);
  const sum = summarize(lines, cfg);
  const [noteAccts, setNoteAccts] = useState(() => st.notes.map((v) => v.l.acct));
  useEffect(() => {   // บรรทัดที่เพิ่งกลายเป็นต้องชี้แจง (เช่น เพิ่งเลือกปีหน้าไม่ใช้) ต่อท้ายรายการ
    const add = st.notes.map((v) => v.l.acct).filter((a) => !noteAccts.includes(a));
    if (add.length) setNoteAccts((x) => [...x, ...add]);
  }, [st.notes.length]);
  const noteRows = noteAccts.map((acct) => lines.find((l) => l.acct === acct)).filter(Boolean)
    .map((l) => ({ l, ev: R.evaluateLine(l, cfg) })).filter((v) => v.ev.noteRequired);
  const notesLeft = noteRows.filter((v) => v.ev.noteMissing).length;
  const sent = data.status === 'submitted' || data.status === 'locked';
  const blind = !!data.refHidden;
  return (
    <Modal title={sent ? 'สรุปงบที่ส่ง' : st.issues ? 'ตรวจก่อนส่ง · เหลือ ' + st.issues + ' เรื่อง' : 'พร้อมส่งงบประมาณ'} wide="xl" onClose={onClose} footer={<>
      <span className="small mute" style={{ marginRight: 'auto', alignSelf: 'center' }}>{canSubmit ? 'ส่งแล้วจะแก้ไขไม่ได้ จนกว่าผู้ดูแลจะตีกลับให้แก้' : ''}</span>
      <button className="btn" onClick={onClose}>{st.issues ? 'ปิด แล้วกรอกต่อ' : 'ยังก่อน'}</button>
      {canSubmit && <button className="btn primary" disabled={busy} onClick={onSubmit}><Icon name="check" />ส่งงบประมาณ</button>}
    </>}>
      <div className="rvsum">
        <div><span>{L.request}</span><b>{fmt(R.round2(sum.R + st.unmapped.amount))}</b>
          {!blind && sum.B > 0 && sum.filled === sum.n && <em>{fmtPct(((sum.R + st.unmapped.amount - sum.B) / sum.B) * 100)} เทียบ {L.budget}</em>}</div>
        <div><span>กิจกรรม {st.nAct} กิจกรรม</span><b>{fmt(st.actAmt)}</b></div>
        <div><span>ค่าใช้จ่ายประจำ</span><b>{fmt(st.recAmt)}</b></div>
        {!blind && <div><span>{L.budget}</span><b className="old">{fmt(sum.B)}</b></div>}
      </div>

      {st.empty.length > 0 && (
        <section className="rvsec">
          <h3><Icon name="alert" />กิจกรรมที่ยังไม่มีรายการค่าใช้จ่าย ({st.empty.length})</h3>
          {st.empty.map((a) => (
            <div key={a.id} className="remrow">
              <span className="rm-name"><b>{a.name}</b><span className="small mute">เพิ่มรายการค่าใช้จ่าย หรือลบกิจกรรมนี้</span></span>
              <button className="btn sm" onClick={() => onEditActivity(a.id)}>เปิดกิจกรรม</button>
            </div>
          ))}
        </section>
      )}
      {st.itemNotes.length > 0 && (
        <section className="rvsec">
          <h3><Icon name="alert" />รายการค่าใช้จ่ายที่ยังไม่ใส่หมายเหตุ ({R.sum(st.itemNotes.map((x) => x.n))} รายการ ใน {st.itemNotes.length} กิจกรรม)</h3>
          {st.itemNotes.map(({ a, n }) => (
            <div key={a.id} className="remrow">
              <span className="rm-name"><b>{a.name}</b><span className="small mute">ยังไม่ใส่หมายเหตุ {n} รายการ — เปิดกิจกรรม กด "แก้ไข" แล้วเล่าว่าเป็นค่าอะไร ทำไมขอยอดนี้ คิดอย่างไร</span></span>
              <button className="btn sm" onClick={() => onEditActivity(a.id)}>เปิดกิจกรรม</button>
            </div>
          ))}
        </section>
      )}
      {st.unmapped.n > 0 && (
        <div className="small mute rvinfo">
          <Icon name="note" />รายการในกิจกรรม {st.unmapped.n} รายการ ({fmt(st.unmapped.amount)} บาท) รอฝ่ายบัญชีจัดเข้ารหัสบัญชี — ส่งงบได้เลย ฝ่ายบัญชีจัดให้ภายหลัง
        </div>
      )}
      {st.recTodo.length > 0 && (
        <section className="rvsec">
          <h3><Icon name="alert" />ค่าใช้จ่ายประจำที่ยังไม่ใส่ยอด ({st.recTodo.length})</h3>
          {st.recTodo.map((r) => (
            <div key={r.acct} className="remrow">
              <span className="rm-name"><b>{r.name}</b><span className="small mute">{r.acct}{r.l.refHidden ? '' : ' · ' + L.forecast + ' ' + (r.ev.P == null ? '–' : fmt(r.ev.P))}</span></span>
              {canEdit && <>
                <button className="btn sm primary" onClick={() => { onClose(); onRecurring(r.acct); }}>ไปใส่ยอด</button>
                <button className="btn sm" onClick={() => onDeclineRec(r.acct)}>ไม่ใช้</button>
              </>}
            </div>
          ))}
        </section>
      )}
      {noteRows.length > 0 && (
        <section className="rvsec">
          <h3><Icon name={notesLeft ? 'alert' : 'checkCircle'} />ต้องใส่คำชี้แจง {notesLeft ? '(เหลือ ' + notesLeft + ')' : '— ครบแล้ว'}</h3>
          {noteRows.map(({ l, ev }) => (
            <div key={l.acct} className="rvnote">
              <div className="rm-name">
                <b>{l.name}</b>
                <span className="small mute">{l.acct}{l.refHidden ? '' : ' · ' + L.budget + ' ' + fmt(ev.B)} · ขอปี {cfg.year} {l.declined ? 'ปีหน้าไม่ใช้' : fmt(ev.R)}</span>
              </div>
              <FlagMsg ev={ev} cfg={cfg} />
              <NoteBox l={l} ev={ev} own src={[]} editing={canEdit} stale={false} showErrors={showErrors} actions={actions} />
            </div>
          ))}
        </section>
      )}
      {!st.issues && !sent && (
        <div className="banner green" style={{ margin: 0, boxShadow: 'none' }}>
          <Icon name="checkCircle" /><div><b>ครบทุกเรื่องแล้ว</b><div className="small">ตรวจตัวเลขด้านบนอีกครั้ง แล้วกด "ส่งงบประมาณ"</div></div>
        </div>
      )}
    </Modal>
  );
}
