// Daily Report — popups: income form, expense form, room picker, delete, lock
const { useState: useStateP, useEffect: useEffectP } = React;
const H = window.HATAARA;

// date helpers — default new transactions to today / today+N (YYYY-MM-DD for <input type="date">)
const todayISO = () => { const d = new Date(); d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); return d.toISOString().slice(0, 10); };
const addDaysISO = (n) => { const d = new Date(); d.setDate(d.getDate() + n); d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); return d.toISOString().slice(0, 10); };
const addDaysFromISO = (isoDate, n) => { try { const d = new Date(isoDate + 'T00:00:00'); d.setDate(d.getDate() + n); d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); return d.toISOString().slice(0, 10); } catch(e) { return addDaysISO(n); } };
const nightsDiff = (ci, co) => { try { const a = new Date(ci + 'T00:00:00'), b = new Date(co + 'T00:00:00'); return Math.max(1, Math.round((b - a) / 86400000)); } catch(e) { return 1; } };
// the table stores/displays DD/MM/YYYY; <input type="date"> needs YYYY-MM-DD. convert both ways.
const toISO = (s) => { const m = /^(\d{2})\/(\d{2})\/(\d{4})/.exec(String(s || '')); return m ? `${m[3]}-${m[2]}-${m[1]}` : (String(s || '').slice(0, 10)); };
const toDisplay = (s) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(s || '')); return m ? `${m[3]}/${m[2]}/${m[1]}` : String(s || ''); };

function Scrim({ children, onClose }) {
  return <div className="scrim" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>{children}</div>;
}

function CardSelect({ value, options, onChange, disabled, ariaLabel, title }) {
  const [open, setOpen] = useStateP(false);
  const label = options.find((option) => (typeof option === 'string' ? option : option.value) === value);
  const display = typeof label === 'string' ? label : (label && label.label) || ariaLabel;
  return <div className={'card-select' + (open ? ' open' : '')}>
    <button type="button" className="card-select-trigger" disabled={disabled} aria-label={ariaLabel} title={title}
      onClick={() => setOpen((shown) => !shown)}>{display}<span className="card-select-chevron">⌄</span></button>
    {open && !disabled && <div className="card-select-menu">
      {options.map((option) => {
        const optionValue = typeof option === 'string' ? option : option.value;
        const optionLabel = typeof option === 'string' ? option : option.label;
        return <button type="button" key={optionValue || 'empty'} className={optionValue === value ? 'selected' : ''}
          onClick={() => { onChange(optionValue); setOpen(false); }}>{optionLabel}</button>;
      })}
    </div>}
  </div>;
}

/* ---------- Income form (INVOICE / NO INVOICE) — Add Transaction layout ----------
   Row 1: Invoice (with INV number inside) | No Invoice — equal boxes
   Row 2: Booking Channel (with Booking ID inside) | Guest Name | Room Number
   Row 3: C/I | C/O — equal size, combined width matches other rows
   Row 4: Total Amount | Payment | Remaining Balance (read-only, derived)
   Row 5: Notes (full width)
   Total Amount is the Charge — editable only when creating a NEW Charge,
   read-only when editing an existing one (a later payment must never
   change the original Charge amount). Remaining Balance is never typed by
   staff; it is always Total Amount minus chainPaidTotal, which is passed
   in from app.jsx (the only place that knows the real chain-aggregated
   payment total via window.PaymentEngine). ---------- */
function IncomeForm({ side: sideProp, edit, draft, onClose, onSave, onPickRoom, pickedRoom, pickedRooms, onDelete, onRoomStatus, onMoveRoom, onStayover, shiftDate, forceTodayCI, userRole, chainOutstanding, onPayChainOutstanding, lockStartDate, chainPaidTotal, chainTotalAmount, onConfirmPayment, engineReady, paymentBusy, paymentError, paymentHistory, paymentGroups, pgGroupTotals, locked, onCorrectPayment, onVoidPayment }) {
  const [side, setSide] = useStateP(sideProp);
  const isInv = side === 'inv';
  const sources = isInv ? H.SOURCES.invoice : H.SOURCES.noinvoice;
  // Default check-in date. When opened from a Dashboard room card
  // (forceTodayCI), the check-in must ALWAYS be today's real date — the
  // Dashboard's selected report/view date must never become the C/I of a
  // newly-created transaction (item 5). Other entry points keep shiftDate.
  const defaultCI = forceTodayCI ? todayISO() : (shiftDate ? toISO(shiftDate) : todayISO());
  const initialRecord = draft || edit;
  const editingExisting = !!(edit && edit._id);
  // Safe amount edit: allowed only when no payment has been received yet,
  // not a Payment Group Member (member amounts are group-level only),
  // not private OTA, and the date is not locked/finalized.
  const recvSoFar = Number(edit?.recv || 0);
  const isMember = !!(edit?.paymentGroupId && !edit?.paymentGroupIsPrimary);
  const canEditAmount = editingExisting && !isMember && !locked;
  const [f, setF] = useStateP(initialRecord
    ? { ...initialRecord,
        totalAmount: initialRecord.totalAmount ?? ((Number(initialRecord.recv || 0) + Number(initialRecord.pendingAmount || 0)) || ''),
        privateOta: !!(initialRecord.privateOta || initialRecord.paymentStatus === 'private_ota'), ci: toISO(initialRecord.ci), co: toISO(initialRecord.co), invoiceStatus: initialRecord.invoiceStatus || (sideProp === 'inv' ? 'pending' : 'not_required') }
    : {
        id: '', cat: 'RO', detail: '', source: sources[0],
        channel: '', guest: '', room: pickedRoom || '',
        ci: defaultCI, co: addDaysFromISO(defaultCI, 1),
        totalAmount: '', privateOta: false, paymentStatus: 'no_payment', invoiceStatus: sideProp === 'inv' ? 'pending' : 'not_required',
        qrTime: '', note: '',
      });
  const [subPopup, setSubPopup] = useStateP(null); // 'sales' | 'payment' | null
  const [viewingPayment, setViewingPayment] = useStateP(null); // a single Payment Record, opened read-only from history
  // ซื้อบิล mode: preserved as a historical compatibility path, but no
  // control in this room-income form can activate it. It is not itself a
  // Charge with a payment lifecycle; its retained submit path produces two
  // finished transactions immediately on save. When active, Room
  // Number/C-I/C-O/Booking Channel/Total Amount/Payment are all replaced by
  // exactly two fields: Reference Room Price and the auto-calculated 30%
  // Bill Service Fee. Toggling it off restores the ordinary form untouched
  // — no extra fields are ever permanently added to the normal income form.
  const [buyBill, setBuyBill] = useStateP(false);
  const [refRoomPrice, setRefRoomPrice] = useStateP('');
  // Payment Group state — only used when creating a NEW record, not when editing
  const [pgMode, setPgMode] = useStateP('none'); // 'none' | 'new' | 'join'
  const [pgJoinId, setPgJoinId] = useStateP('');
  // P2: must be confirmed by user before saving as Member — blocks saveDisabled until true
  const [pgTotalConfirmed, setPgTotalConfirmed] = useStateP(false);
  // Controls fallback "show all groups" mode in join list (starts collapsed to same-date only)
  const [pgShowAll, setPgShowAll] = useStateP(false);
  const refPriceNum = Number(refRoomPrice || 0);
  const serviceFee = Math.round(refPriceNum * 0.3 * 100) / 100; // 30% of Reference Room Price, auto-calculated only
  const refPriceInvalid = buyBill && (!Number.isFinite(refPriceNum) || refPriceNum <= 0);
  const fmtMoney = (n) => Number(n || 0).toLocaleString('en-US');

  const set = (k, v) => setF((s) => ({ ...s, [k]: v }));
  const handleCI = (newCI) => { const n = nightsDiff(f.ci, f.co); setF(s => ({ ...s, ci: newCI, co: addDaysFromISO(newCI, n) })); };
  const nights = nightsDiff(f.ci, f.co);
  const switchSide = (s) => {
    if (s === side) return;
    setSide(s);
    const newSources = s === 'inv' ? H.SOURCES.invoice : H.SOURCES.noinvoice;
    const supportedMethods = s === 'inv' ? ['Cash', 'QR', 'EDC', 'Key-in'] : ['Cash', 'QR', 'BBL', 'AGP'];
    setF((prev) => {
      const keepSrc = newSources.includes(prev.source) ? prev.source : newSources[0];
      const keepChannel = supportedMethods.includes(prev.channel) ? prev.channel : '';
      return { ...prev, source: keepSrc, channel: keepChannel, qrTime: s === 'noinv' && keepChannel === 'QR' ? prev.qrTime : '',
        id: '', bookingId: keepSrc === prev.source ? prev.bookingId : '', invoiceStatus: s === 'inv' ? 'pending' : 'not_required' };
    });
  };
  const room = pickedRoom != null ? pickedRoom : f.room;
  // Multi-room selection from RoomPicker when creating a new transaction.
  // Active whenever pickedRooms has items (multi-select returns rooms[] even for 1 room).
  // pgMode is no longer required — rooms are populated directly from picker output.
  const pgRoomsList = !editingExisting && Array.isArray(pickedRooms) && pickedRooms.length > 0
    ? pickedRooms : [];
  const displayRoom = pgRoomsList.length > 1 ? pgRoomsList.join(', ') : (room || '');
  // OTA_SOURCES: channels that require a Booking ID.
  // 'Agoda INV' and 'Agoda NoINV' kept for backward compatibility with existing saved records.
  const OTA_SOURCES = ['Agoda', 'Agoda INV', 'Agoda NoINV', 'Ascend', 'Webbeds', 'Booking.com', 'Expedia', 'Ctrip', 'Thai Tour'];
  const ota = OTA_SOURCES.includes(f.source);
  const isOwner = userRole === 'owner';
  const totalAmount = Number(f.totalAmount || 0);
  // Remaining Balance is ALWAYS derived — never a field staff can type into,
  // here or anywhere else. For an existing Charge, it must reflect the
  // WHOLE stay chain's outstanding balance (e.g. Night 1 750 + Night 2 750
  // = 1,500 total, minus whatever has been paid across the chain so far),
  // not just the single Charge record currently open — otherwise paying
  // the full chain balance in one shot would be incorrectly blocked as an
  // "overpayment" against only one night's amount. chainTotalAmount and
  // chainPaidTotal are both supplied by app.jsx, which is the only place
  // that can see every record in the chain plus the real Payment Record
  // log (via window.PaymentEngine). For a brand-new Charge being created
  // right now (not yet saved, no chain to belong to), it is Total Amount
  // minus whatever payment was just confirmed in the Payment popup this
  // session (f._initialPaymentAmount — see handlePaymentConfirm below).
  // That value is not yet a real Payment Record (the Charge does not
  // exist in storage yet), so it cannot come from chainPaidTotal; it must
  // be read from the form's own pending-save state instead.
  const remainingBalance = canEditAmount
    ? Math.max(0, totalAmount - recvSoFar)  // estimate pending after amount correction
    : editingExisting
      ? Math.max(0, Number(chainTotalAmount || 0) - Number(chainPaidTotal || 0))
      : Math.max(0, totalAmount - Number(f._initialPaymentAmount || 0));
  // Item 3: a draft initial payment was entered for a brand-new Charge, but
  // the PaymentEngine bridge is not ready — so this payment cannot actually
  // be committed on save. Used to visually flag the summary card and avoid a
  // false "paid" appearance. engineReady === false only (undefined → allowed).
  const engineNotReadyWithPending = engineReady === false && !editingExisting
    && !f.privateOta && Number(f._initialPaymentAmount || 0) > 0;
  const invoiceStatus = isInv ? (String(f.id || '').trim() ? 'provided' : 'pending') : 'not_required';
  const requiredErrors = {
    guest: !String(f.guest || '').trim() ? 'กรุณากรอกชื่อลูกค้า' : '',
    room: !room ? 'กรุณาเลือกห้อง' : '',
    dates: !(new Date(f.ci) < new Date(f.co)) ? 'C/O ต้องอยู่หลัง C/I' : '',
    // amount only applies when creating a brand-new Charge — an existing
    // Charge's totalAmount is fixed/read-only and was already validated
    // the moment it was first created.
    // P1: Member rooms (pgMode === 'join') do not require a total amount —
    // their payment is tracked at group level via the Primary room only.
    amount: !editingExisting && !f.privateOta && pgMode !== 'join'
      && (!Number.isFinite(totalAmount) || totalAmount <= 0) ? 'กรุณากรอกยอดเงินรวม' : '',
  };
  const requiredInvalid = Object.values(requiredErrors).some(Boolean);
  const otaInvalid = ota && !String(f.bookingId || '').trim();
  // ซื้อบิล mode replaces every normal requirement (guest/room/dates/amount)
  // with exactly one: Reference Room Price must be a positive number. Room
  // Number, C/I, C/O, and Booking Channel are intentionally not required —
  // per spec, ซื้อบิล does not connect to a room, stay chain, or room
  // status at all.
  // P2: block save when join mode is incomplete or group total not yet confirmed
  const pgJoinIncomplete = pgMode === 'join' && !pgJoinId;
  const pgJoinUnconfirmed = pgMode === 'join' && !!pgJoinId && !pgTotalConfirmed;
  // Safe amount edit: block save if unlocked-for-edit but new amount is 0/empty
  const amountEditInvalid = canEditAmount && (!Number.isFinite(totalAmount) || totalAmount <= 0);
  const saveDisabled = buyBill ? refPriceInvalid
    : (requiredInvalid || otaInvalid || pgJoinIncomplete || pgJoinUnconfirmed || amountEditInvalid);
  const fieldError = (bad) => bad ? ' field-invalid' : '';
  const selectSource = (value) => setF((prev) => ({ ...prev, source: value, bookingId: OTA_SOURCES.includes(value) ? prev.bookingId : '' }));

  // Confirming a payment from the Payment popup. For a NEW Charge (not yet
  // saved — no _id), there is nothing to settle against yet: the payment
  // simply becomes that Charge's initial received amount on save, same as
  // before. For an EXISTING Charge, this calls up to app.jsx, which runs
  // the real backend settlement engine (FIFO, overpayment/duplicate
  // rejection, atomic rollback) and only updates state on ok:true.
  const handlePaymentConfirm = (payment) => {
    if (editingExisting) {
      if (Number(payment.amount || 0) > 0) {
        // Settlement: actual new payment received → run full payment engine flow.
        onConfirmPayment(edit, payment);
      }
      // If amount === 0 this is a pure totalAmount correction (amountOnlyEdit).
      // Do NOT create a paymentRecord with amount 0.
      // The edited totalAmount is already in f.totalAmount via onAmountChange;
      // main form saveIncome (amountEdit path) will persist recv/pending/status.
      // Nothing else to do here — popup will close via setSubPopup(null).
    } else {
      // No Charge exists yet — record the intended initial channel/amount
      // on the form so submit() can pass it through on creation. This is
      // NOT a second payment path: it is the same single api('payment',...)
      // call that already fires today when a brand-new record is saved
      // with received > 0 (see saveIncome in app.jsx).
      setF((prev) => ({ ...prev, channel: payment.channel, _initialPaymentAmount: payment.amount, qrTime: payment.qrTime || '' }));
    }
  };

  const submit = () => {
    if (saveDisabled) return;
    if (buyBill) {
      // ซื้อบิล: the document the customer purchased is classified as
      // INV (carries the Reference Room Price); the 30% Bill Service Fee
      // is a SEPARATE NO INV income transaction. Both are built here and
      // handed to onSave together as a pair — app.jsx (saveIncome) is
      // responsible for writing both records atomically. Neither record
      // gets a room, C/I, C/O, stayChainId, or parent_booking_id — per
      // spec this income type never connects to a room or stay chain.
      // Payment History/Remaining Balance do not apply here either: both
      // records are created already-settled finished transactions, not
      // open Charges, matching "the service-fee transaction must... count
      // toward daily income" with no further payment lifecycle implied.
      const invDoc = {
        id: f.id || '', cat: 'RO', detail: 'ซื้อบิล', source: 'Walk-in',
        channel: f.channel || '', guest: f.guest || '',
        room: '', ci: '', co: '',
        recv: refPriceNum, pendingAmount: 0,
        privateOta: false, paymentStatus: 'received', invoiceStatus: String(f.id || '').trim() ? 'provided' : 'pending',
        qrTime: f.qrTime || '', note: f.note || '',
        buyBillRefPrice: refPriceNum,
      };
      const feeDoc = {
        id: '', cat: 'RO', detail: 'ค่าบริการซื้อบิล 30%', source: 'Walk-in',
        channel: f.channel || '', guest: f.guest || '',
        room: '', ci: '', co: '',
        recv: serviceFee, pendingAmount: 0,
        privateOta: false, paymentStatus: 'received', invoiceStatus: 'not_required',
        qrTime: f.qrTime || '', note: 'ค่าบริการ 30% จากยอดอ้างอิง ' + fmtMoney(refPriceNum),
        buyBillServiceFeeOf: null, // linked by app.jsx after the INV doc's _id is assigned
      };
      onSave({ buyBill: true, invDoc, feeDoc }, 'inv', { buyBill: true });
      return;
    }
    if (ota && !String(f.bookingId || '').trim()) { alert('Booking ID บังคับสำหรับ OTA — กรุณากรอกก่อนบันทึก'); return; }
    if (editingExisting) {
      // Editing an existing Charge. Two sub-cases:
      // (A) canEditAmount=true (not Member, not locked): allow correcting
      //     totalAmount — app.jsx recalculates recv/pendingAmount/paymentStatus
      //     from live paymentRecords, so we only send the new total here.
      // (B) canEditAmount=false: non-financial fields only. recv/totalAmount/
      //     pendingAmount/paymentStatus are NEVER touched — they are owned exclusively
      //     by the payment-engine settlement flow (onConfirmPayment in app.jsx).
      const output = { ...f, invoiceStatus, room, ci: toDisplay(f.ci), co: toDisplay(f.co) };
      delete output.extraItems;
      delete output.paymentMode;
      delete output._initialPaymentAmount;
      if (canEditAmount) {
        const newTotal = Number(f.totalAmount || 0);
        output.totalAmount = newTotal;
        // Do not set recv/pendingAmount/paymentStatus here.
        // app.jsx (saveIncome amountEdit path) will recalculate from paymentRecords.
        delete output.recv;
        delete output.pendingAmount;
        delete output.paymentStatus;
        onSave(output, side, { nonFinancialOnly: true, amountEdit: true });
      } else {
        delete output.totalAmount;
        delete output.recv;
        delete output.pendingAmount;
        delete output.paymentStatus;
        onSave(output, side, { nonFinancialOnly: true });
      }
      return;
    }
    const initialPaid = Number(f._initialPaymentAmount || 0);
    const output = { ...f, totalAmount,
      recv: f.privateOta ? 0 : initialPaid,
      pendingAmount: f.privateOta ? 0 : Math.max(0, totalAmount - initialPaid),
      privateOta: isOwner && !!f.privateOta, invoiceStatus, room, ci: toDisplay(f.ci), co: toDisplay(f.co) };
    output.paymentStatus = output.recv > 0 && output.pendingAmount > 0 ? 'partial_deposit'
      : output.recv > 0 ? 'received'
      : output.pendingAmount > 0 ? 'pending_payment'
      : 'no_payment';
    delete output.extraItems;
    delete output.paymentMode;
    delete output._initialPaymentAmount;
    // Attach Payment Group intent — app.jsx (saveIncome) will create the group.
    // Auto PG Phase 2: group is triggered by rooms count, not pgMode.
    // pickedRooms.length > 1 → batch creation (Primary = pickedRooms[0], Members = rest).
    // pickedRooms.length <= 1 → normal single-room transaction, no group.
    const autoGroup = !editingExisting && !buyBill
      && Array.isArray(pickedRooms) && pickedRooms.length > 1;
    if (autoGroup) {
      output.paymentGroupMode = 'new';
      output.paymentGroupRooms = pickedRooms; // pickedRooms[0] is Primary by convention
    } else if (pgMode === 'join' && pgJoinId) {
      // Legacy join path — kept for backward compatibility with old saved groups.
      // No UI to trigger this anymore; dead code until Phase 4 cleanup.
      output.paymentGroupMode = 'join';
      output.paymentGroupJoinId = pgJoinId;
    }
    onSave(output, side);
  };

  return (
    <Scrim onClose={onClose}>
      <div className="popup income-popup" onMouseDown={(e) => e.stopPropagation()}>
        <div className="popup-head">
          <h3>{edit ? 'แก้ไขรายการ' : 'เพิ่มรายการ' + (room ? ' · ห้อง ' + room : '')}</h3>
          <button className="popup-x" aria-label="ปิด" title="ปิด" onClick={onClose}>×</button>
        </div>
        <div className="form-scroll"><div className="form-grid income-form-grid">

          {/* Row 1 — Invoice (with INV number inside) | No Invoice.
              The historical ซื้อบิล data path remains supported by the
              preserved buyBill state/submit logic, but this room-income
              form no longer exposes a control that can enter that mode. */}
          <div className="fld full doc-type-row">
            <div className="doc-choice-grid">
              <div className={'doc-choice-card' + (isInv && !buyBill ? ' on' : '')}
                role="button" tabIndex={0} aria-pressed={isInv && !buyBill}
                onClick={() => { setBuyBill(false); switchSide('inv'); }}
                onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setBuyBill(false); switchSide('inv'); } }}>
                <span className="doc-choice-label">INV</span>
                {isInv && !buyBill && (
                  <input id="room-income-invoice-number" className="mono invoice-number-input" aria-label="เลข INV" inputMode="numeric"
                    value={f.id || ''} onChange={(e) => { e.stopPropagation(); set('id', e.target.value.replace(/\D/g, '')); }}
                    onClick={(e) => e.stopPropagation()}
                    placeholder="กรอกเลข INV" />
                )}
              </div>
              <div className={'doc-choice-card' + (!isInv && !buyBill ? ' on' : '')}
                role="button" tabIndex={0} aria-pressed={!isInv && !buyBill}
                onClick={() => { setBuyBill(false); switchSide('noinv'); }}
                onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setBuyBill(false); switchSide('noinv'); } }}>
                <span className="doc-choice-label">NO INV</span>
              </div>
            </div>
          </div>

          {buyBill ? (
            /* ซื้อบิล fields — shown ONLY when this mode is active. Room
               Number, C/I, C/O, Booking Channel, Total Amount, and Payment
               are all intentionally absent: ซื้อบิล does not require a
               room, does not belong to a stay chain, and produces two
               already-settled transactions directly on save (see submit
               above), not an open Charge with its own payment lifecycle. */
            <div className="fld full row2-3col buy-bill-row">
              <div className={'fld inside-fld' + (refPriceInvalid ? ' field-invalid' : '')}>
                <input aria-label="ยอดอ้างอิงราคาห้อง" className="mono" type="number" min="0"
                  value={refRoomPrice} onChange={(e) => setRefRoomPrice(e.target.value)}
                  placeholder="ยอดอ้างอิงราคาห้อง" title={refPriceInvalid ? 'กรุณากรอกยอดอ้างอิงมากกว่า 0' : ''} />
              </div>
              <div className="fld remaining-balance-fld buy-bill-fee-fld">
                <span className="remaining-balance-lbl">ค่าบริการ 30%</span>
                <span className="remaining-balance-amt mono">{fmtMoney(serviceFee)}</span>
              </div>
            </div>
          ) : (<>

          {/* Row 2 — Booking Channel | Booking ID | Room Number */}
          <div className="fld full row2-3col">
            <div className="fld ch-field">
              <button type="button" className={'ch-trigger ch-full' + (f.source ? '' : ' ch-placeholder')}
                aria-label="ช่องทางการจอง"
                onClick={() => setSubPopup('sales')}>
                {f.source || 'Walk-in'}<span className="card-select-chevron">⌄</span>
              </button>
            </div>
            <div className={'fld inside-fld' + fieldError(otaInvalid)}>
              <input className="mono" aria-label="Booking ID" disabled={!ota}
                value={f.bookingId || ''} onChange={(e) => set('bookingId', e.target.value)}
                placeholder="Booking ID" title={otaInvalid ? 'กรุณากรอก Booking ID' : ''} />
            </div>
            <div className={'fld inside-fld' + fieldError(requiredErrors.room)}>
              <button className="room-pick-btn" aria-label="เลขห้อง"
                onClick={() => onPickRoom(f, side, { multiSelect: !editingExisting })}
                title={requiredErrors.room}>
                {pgRoomsList.length > 1
                  ? ('ห้อง · ' + pgRoomsList.join(', ') + ' (' + pgRoomsList.length + ' ห้อง)')
                  : ('เลขห้อง · ' + (displayRoom || 'เลือกห้อง…'))}
              </button>
            </div>
          </div>

          {/* Row 3 — Guest Name | C/I | C/O */}
          <div className="fld full row2-3col">
            <div className={'fld inside-fld' + fieldError(requiredErrors.guest)}>
              <input aria-label="ชื่อลูกค้า" value={f.guest} onChange={(e) => set('guest', e.target.value)} placeholder="ชื่อลูกค้า" title={requiredErrors.guest} />
            </div>
            <div className="fld inside-fld">
              <input aria-label="C/I" className="mono" type="date" value={f.ci} disabled={lockStartDate} title={lockStartDate ? 'วันที่เริ่มถูกกำหนดจากการ์ดห้องที่เลือก' : ''} onChange={(e) => handleCI(e.target.value)} />
            </div>
            <div className={'fld inside-fld' + fieldError(requiredErrors.dates)}>
              <input aria-label={'C/O · ' + nights + ' คืน'} title={requiredErrors.dates} className="mono" type="date" value={f.co} onChange={(e) => set('co', e.target.value)} />
            </div>
          </div>

          {/* Row 4 — ชำระเงิน summary card (1 col) | หมายเหตุ note card (2 col) */}
          <div className="fld full charge-note-row">
            {/* Left: Payment summary card — single clickable, no nested boxes.
                When a draft initial payment exists but the PaymentEngine is
                not ready (item 3), the card must NOT read as a completed green
                payment — it switches to a warning state so the green summary
                never falsely implies success. */}
            <button type="button"
              className={'charge-summary-card' + (f.privateOta ? ' disabled' : '') + (paymentBusy ? ' disabled' : '') + (engineNotReadyWithPending ? ' engine-blocked' : '')}
              disabled={!!f.privateOta || paymentBusy}
              onClick={() => setSubPopup('payment')}>
              <span className="cscard-label">ชำระเงิน</span>
              {(paymentBusy || f.privateOta || totalAmount > 0) && (
                <span className="cscard-detail">
                  {paymentBusy ? 'กำลังบันทึก…'
                    : f.privateOta ? 'Private OTA'
                    : engineNotReadyWithPending ? 'ระบบรับชำระยังไม่พร้อม · ยังไม่บันทึกยอดรับ'
                    : `ค่าห้อง ฿${fmtMoney(totalAmount)}${f.channel ? ' · ' + (f.channel === 'Key-in' ? 'KEY-IN' : f.channel) + (Number(f._initialPaymentAmount||0) > 0 ? ' ฿'+fmtMoney(f._initialPaymentAmount) : '') : ''} · ค้าง ฿${fmtMoney(remainingBalance)}`}
                </span>
              )}
            </button>
            {/* Right: Note card — 2 grid cols */}
            <div className="note-card">
              <input aria-label="หมายเหตุ" className="note-card-input" value={f.note} onChange={(e) => set('note', e.target.value)} placeholder="หมายเหตุ" />
            </div>
          </div>
          {paymentError && <div className="fld full"><div className="payment-error-banner">{paymentError}</div></div>}
          </>)}

          {/* Payment Group UI removed — Auto PG Phase 1.
              Group is now created automatically when user selects more than one room
              in RoomPicker. No manual สร้างกลุ่มใหม่ / เข้าร่วมกลุ่ม buttons. */}
          {/* Editing: show existing group info read-only */}
          {editingExisting && edit && edit.paymentGroupCode && !buyBill && (
            <div className="fld full pg-group-row">
              <span className="pg-group-label">กลุ่มชำระ</span>
              <span className={'pg-badge ' + (edit.paymentGroupIsPrimary ? 'pg-primary' : 'pg-member')}>{edit.paymentGroupCode}</span>
              <span className="pg-group-role">{edit.paymentGroupIsPrimary ? '· Primary' : '· Member'}</span>
            </div>
          )}
          {/* Payment History — full-width, read-only, below Notes. Only relevant
              once this Charge exists (no payment events on an unsaved Charge).
              Sorted newest-first; each row opens the record read-only. */}
          {editingExisting && (
            <div className="fld full payment-history-card">
              <div className="payment-history-title">ประวัติการชำระเงิน</div>
              {(!paymentHistory || paymentHistory.length === 0) ? (
                <div className="payment-history-empty">ยังไม่มีการชำระเงิน</div>
              ) : (
                <div className="payment-history-list">
                  {paymentHistory.map((p) => {
                    const dt = p.createdAt ? new Date(p.createdAt) : null;
                    const timeStr = dt && !isNaN(dt) ? dt.toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit' }) : '—';
                    return (
                      <button type="button" key={p._id || p.paymentId} className="payment-history-row" onClick={() => setViewingPayment(p)}>
                        <span className="phr-date mono">{p.paymentDate || '—'}</span>
                        <span className="phr-time mono">{timeStr}</span>
                        <span className="phr-amount mono">{fmtMoney(p.amount)}</span>
                        <span className="phr-method">{p.channel === 'Key-in' ? 'KEY-IN' : (p.channel || '—')}</span>
                        <span className="phr-user">{p.createdBy || '—'}</span>
                      </button>
                    );
                  })}
                </div>
              )}
            </div>
          )}
        </div></div>
        <div className="popup-foot">
          {onDelete && <button className="pbtn del-ghost" style={{ marginRight: 'auto' }} onClick={onDelete}>ลบรายการ</button>}
          {edit && room && chainOutstanding && <button className="pbtn primary orange" onClick={onPayChainOutstanding}>ชำระยอดค้างอยู่ต่อ ({fmtPlain ? fmtPlain(chainOutstanding.pendingAmount) : chainOutstanding.pendingAmount})</button>}
          {edit && room && <button className="pbtn ghost" onClick={() => onRoomStatus(room)}>สถานะห้อง</button>}
          {edit && room && <button className="pbtn primary" disabled={locked} title={locked ? 'ปิดรอบแล้ว ไม่สามารถย้ายห้องได้' : ''} onClick={onMoveRoom}>ย้ายห้อง</button>}
          {edit && room && <button className="pbtn primary green" onClick={onStayover}>อยู่ต่อ</button>}
          <button className="pbtn ghost" onClick={onClose}>ยกเลิก</button>
          <button className="pbtn primary" disabled={saveDisabled} onClick={submit}>{edit ? 'อัปเดต' : 'บันทึก'}</button>
        </div>
      </div>
      {subPopup === 'sales' &&
        <SalesPopup
          sources={sources}
          current={f.source}
          onConfirm={(src) => selectSource(src)}
          onClose={() => setSubPopup(null)}
        />}
      {subPopup === 'payment' &&
        <PaymentPopup
          isInv={isInv}
          privateOta={f.privateOta}
          initChannel={f.channel}
          maxAmount={remainingBalance}
          totalAmount={totalAmount}
          remainingBalance={remainingBalance}
          canEditAmount={!f.privateOta && !isMember && (!editingExisting || canEditAmount)}
          amountDisabledReason={
            f.privateOta ? 'Private OTA ไม่สามารถแก้ยอดได้'
              : isMember ? 'ห้อง Member ไม่สามารถแก้ยอดแยกจากกลุ่มได้'
              : (editingExisting && !canEditAmount) ? 'ปิดรอบแล้ว ไม่สามารถแก้ยอดได้'
              : ''
          }
          currentTotalAmount={f.totalAmount}
          engineReady={engineReady}
          onAmountChange={(v) => set('totalAmount', v)}
          onClear={() => {
            if (!editingExisting) {
              // New transaction: reset all unsaved payment draft fields so
              // the payment summary card returns to empty/default state.
              setF((prev) => ({
                ...prev,
                totalAmount: '',
                channel: '',
                _initialPaymentAmount: 0,
                qrTime: '',
              }));
            }
            // Existing transaction: do not reset stored payment data.
            // Real paymentRecords are not touched; only popup local state
            // (ch/recv/qt) is cleared by handleClear in PaymentPopup.
          }}
          onConfirm={handlePaymentConfirm}
          onClose={() => setSubPopup(null)}
        />}
      {viewingPayment &&
        <PaymentRecordView record={viewingPayment} locked={locked} onClose={() => setViewingPayment(null)}
          onCorrect={onCorrectPayment} onVoid={onVoidPayment} />}
    </Scrim>
  );
}

/* ---------- Booking channel popup (centred, replaces CardSelect for channel) ---------- */
function SalesPopup({ sources, current, onConfirm, onClose }) {
  return (
    <Scrim onClose={onClose}>
      <div className="popup sp-popup" onMouseDown={(e) => e.stopPropagation()}>
        <div className="popup-head">
          <h3>ช่องทางการจอง</h3>
          <button className="popup-x" aria-label="ปิด" onClick={onClose}>×</button>
        </div>
        <div className="sp-list">
          {sources.map((src) => (
            <button key={src} className={'sp-opt' + (src === current ? ' sp-opt-on' : '')}
              onClick={() => { onConfirm(src); onClose(); }}>
              {src}
            </button>
          ))}
        </div>
      </div>
    </Scrim>
  );
}

/* ---------- Payment popup (centred, handles method + amount received this
   time + QR time). This popup records ONE payment event only — it does
   NOT know or show Remaining Balance. Remaining Balance is always derived
   on the Add Transaction page itself from Total Charges − Total Payments;
   staff never type it directly anywhere, including here. ---------- */
function PaymentPopup({ isInv, privateOta, initChannel, maxAmount, totalAmount, remainingBalance, canEditAmount, amountDisabledReason, currentTotalAmount, engineReady, onAmountChange, onConfirm, onClear, onClose }) {
  const methods = isInv ? ['Cash', 'QR', 'EDC', 'Key-in'] : ['Cash', 'QR', 'BBL', 'AGP'];
  const [ch, setCh]   = useStateP(initChannel || '');
  const [recv, setRecv] = useStateP('');
  const [qt, setQt]   = useStateP('');

  const recvNum = Number(recv || 0);
  // For new transactions, effectiveMax comes from currentTotalAmount (live-updated via onAmountChange).
  // For existing transactions, maxAmount (remainingBalance from parent) is used.
  const liveTotal = Number(currentTotalAmount || 0);
  const effectiveMax = canEditAmount && liveTotal > 0 ? liveTotal : Number.isFinite(maxAmount) ? maxAmount : Infinity;
  // amountOnlyEdit: user changed totalAmount but entered no new received amount.
  // For existing editable transactions, this is a pure totalAmount correction —
  // no new paymentRecord should be created; main form save handles the recalc.
  const amountOnlyEdit = canEditAmount && liveTotal > 0 && recvNum <= 0;
  // เวลา QR is relevant only for NO INVOICE + QR (existing rule, preserved).
  // Shown as soon as QR is picked so the flow reads top-to-bottom without a
  // permanently reserved dead card.
  const showQr = !isInv && ch === 'QR';
  const needQr = showQr && recvNum > 0;
  const channelErr = !amountOnlyEdit && recvNum > 0 && !ch;
  const emptyErr = !amountOnlyEdit && recvNum <= 0;
  const overpayErr = !amountOnlyEdit && Number.isFinite(effectiveMax) && effectiveMax < Infinity && recvNum > effectiveMax + 0.01;
  const qrErr = needQr && !String(qt).trim();
  const invalid = privateOta ? false : channelErr || emptyErr || qrErr || overpayErr;
  // Engine-not-ready guard (item 3): if a real payment (recvNum > 0) is being
  // confirmed while the PaymentEngine bridge is down, block Confirm so the
  // main form never stamps a false "paid" green summary it cannot save.
  // engineReady is passed from app.jsx; undefined defaults to allowed so this
  // never blocks in contexts that don't wire it.
  const engineBlocked = engineReady === false && !privateOta && !amountOnlyEdit && recvNum > 0;

  const handleMethod = (m) => { setCh(m); if (m !== 'QR') setQt(''); if (!m) setRecv(''); };
  const handleClear = () => { setCh(''); setRecv(''); setQt(''); if (onClear) onClear(); };
  const handleConfirm = () => {
    if (invalid || engineBlocked) return;
    onConfirm({ channel: ch, amount: recvNum, qrTime: ch === 'QR' ? qt : '' });
    onClose();
  };

  return (
    <Scrim onClose={onClose}>
      <div className="popup pp-popup" onMouseDown={(e) => e.stopPropagation()}>
        <div className="popup-head">
          <h3>ชำระเงิน</h3>
          <button className="popup-x" aria-label="ปิด" onClick={onClose}>×</button>
        </div>
        {/* Top-to-bottom flow (no zigzag): 1 ค่าห้อง → 2 วิธีชำระเงิน →
            3 ยอดรับ (+ เวลา QR when QR) → 4 ยอดค้าง → ยืนยัน */}
        <div className="pp-body pp-body-flow">
          {/* Step 1 — ค่าห้อง */}
          <div className="pp-step">
            <div className={'pp-card pp-card-wide' + (!canEditAmount || !!privateOta ? ' pp-card-muted' : '')}>
              <span className="pp-card-lbl">1 · ค่าห้อง</span>
              <div className="pp-card-inner">
                <input className="mono pp-card-input" type="number" min="0"
                  disabled={!canEditAmount || !!privateOta}
                  title={amountDisabledReason || ''}
                  value={currentTotalAmount}
                  onChange={(e) => onAmountChange && onAmountChange(e.target.value)}
                  placeholder="0" />
              </div>
            </div>
          </div>

          {/* Step 2 — วิธีชำระเงิน */}
          <div className="pp-step">
            <span className="pp-step-lbl">2 · วิธีชำระเงิน</span>
            <div className="pp-method-row">
              {methods.map((m) => (
                <button key={m} disabled={!!privateOta}
                  className={'pp-method-card' + (ch === m ? ' on' : '')}
                  onClick={() => handleMethod(m)}>
                  {m === 'Key-in' ? 'KEY-IN' : m}
                </button>
              ))}
            </div>
            {channelErr && <span className="pp-step-err">กรุณาเลือกวิธีชำระเงิน</span>}
          </div>

          {/* Step 3 — ยอดรับ (+ เวลา QR when QR selected) */}
          <div className="pp-step">
            <div className={'pp-amount-grid' + (showQr ? ' has-qr' : '')}>
              <div className={'pp-card pp-card-wide' + (!ch ? ' pp-card-muted' : '') + (channelErr || overpayErr ? ' pp-card-err' : '')}>
                <span className="pp-card-lbl">3 · ยอดรับ</span>
                <div className="pp-card-inner">
                  <input className="mono pp-card-input" type="number" min="0"
                    disabled={!!privateOta || !ch}
                    value={recv}
                    onChange={(e) => setRecv(e.target.value)}
                    placeholder="0" />
                </div>
                {overpayErr && <span className="pp-card-errmsg">เกิน {Number(effectiveMax).toLocaleString('en-US')}</span>}
              </div>
              {showQr && (
                <div className={'pp-card pp-card-wide' + (qrErr ? ' pp-card-err' : '')}>
                  <span className="pp-card-lbl">เวลา QR</span>
                  <div className="pp-card-inner">
                    <input className="mono pp-card-input pp-card-input-text" type="text"
                      value={qt}
                      onChange={(e) => setQt(e.target.value)}
                      placeholder="--:--" />
                  </div>
                </div>
              )}
            </div>
          </div>

          {/* Step 4 — ยอดค้าง (read-only) */}
          <div className="pp-step">
            <div className="pp-card pp-card-wide pp-card-balance due">
              <span className="pp-card-lbl">ยอดค้าง</span>
              <div className="pp-card-inner pp-card-inner-value">
                <span className="mono pp-card-value">
                  {typeof remainingBalance === 'number' ? remainingBalance.toLocaleString('en-US') : remainingBalance}
                </span>
              </div>
            </div>
          </div>

          {engineBlocked && (
            <div className="pp-engine-warn">ระบบรับชำระยังไม่พร้อมใช้งาน กรุณารอสักครู่แล้วลองใหม่ (หรือกด “ล้าง” เพื่อบันทึกเป็นยอดค้างก่อน)</div>
          )}
        </div>
        <div className="popup-foot">
          <button className="pbtn ghost" style={{ marginRight: 'auto' }} onClick={handleClear}>ล้าง</button>
          <button className="pbtn ghost" onClick={onClose}>ยกเลิก</button>
          <button className="pbtn primary" disabled={!!invalid || engineBlocked} onClick={handleConfirm}>ยืนยัน</button>
        </div>
      </div>
    </Scrim>
  );
}

/* ---------- Payment Record view (read-only). Opened by clicking a row in
   the Payment History card. Shows the original payment exactly as it was
   recorded — never editable here, and opening it never touches the Charge
   or creates another payment. If the payment was a stay-chain payment that
   spanned multiple Charges (allocations.length > 1), each allocation line
   is shown so the split is fully visible. ---------- */
function PaymentRecordView({ record, locked, onClose, onCorrect, onVoid }) {
  const fmtMoney = (n) => Number(n || 0).toLocaleString('en-US');
  const dt = record.createdAt ? new Date(record.createdAt) : null;
  const timeStr = dt && !isNaN(dt) ? dt.toLocaleTimeString('th-TH', { hour: '2-digit', minute: '2-digit' }) : '—';
  const allocations = Array.isArray(record.allocations) ? record.allocations : [];
  // Multi-alloc = cross-night FIFO payment; amount edit blocked for these
  const isMultiAlloc = allocations.length > 1;
  const channels = record.docType === 'no_invoice'
    ? ['Cash', 'QR', 'BBL', 'AGP'] : ['Cash', 'QR', 'EDC', 'Key-in'];

  // Effective lock: dashboard date locked, OR payment record's own date locked, OR PG member
  const effectiveLocked = locked || !!record._recordLocked || !!record._isMember;
  // Human-readable reason for read-only state
  const lockedReason = record._isMember
    ? ' (ห้อง Member)'
    : record._recordLocked
      ? ' (วันที่ชำระถูกปิดแล้ว)'
      : locked
        ? ' (ล็อคแล้ว)'
        : '';

  const [editAmt, setEditAmt] = useStateP(String(record.amount || ''));
  const [editChannel, setEditChannel] = useStateP(record.channel || '');
  const [editQrTime, setEditQrTime] = useStateP(record.qrTime || '');
  const [confirmVoid, setConfirmVoid] = useStateP(false);
  const [dirty, setDirty] = useStateP(false);

  const handleSave = () => {
    if (!onCorrect) return;
    const correction = {};
    const newAmt = Number(editAmt);
    // Amount validation guards
    if (!isMultiAlloc && newAmt !== Number(record.amount)) {
      if (isNaN(newAmt) || newAmt <= 0) {
        alert('ยอดรับต้องมากกว่า 0 กรุณาตรวจสอบและแก้ไขใหม่');
        return;
      }
      const bookingTotal = Number(record._bookingTotal || 0);
      if (bookingTotal > 0 && newAmt > bookingTotal) {
        alert(`ยอดรับ (${fmtMoney(newAmt)}) เกินยอดรวมของ Booking (${fmtMoney(bookingTotal)}) ไม่สามารถบันทึกได้`);
        return;
      }
      correction.amount = newAmt;
    }
    if (editChannel !== (record.channel || '')) correction.channel = editChannel;
    if (editChannel === 'QR' && editQrTime !== (record.qrTime || ''))
      correction.qrTime = editQrTime;
    else if (editChannel !== 'QR')
      correction.qrTime = '';
    if (Object.keys(correction).length > 0) onCorrect(record.paymentId, correction, record);
    onClose();
  };

  const handleVoid = () => {
    if (!onVoid) return;
    onVoid(record.paymentId, record);
    onClose();
  };

  return (
    <Scrim onClose={onClose}>
      <div className="popup pay-record-view" onMouseDown={(e) => e.stopPropagation()}>
        <div className="popup-head">
          <h3>รายการชำระเงิน{effectiveLocked ? lockedReason || ' (ล็อคแล้ว)' : ''}</h3>
          <button className="popup-x" aria-label="ปิด" title="ปิด" onClick={onClose}>×</button>
        </div>
        <div className="pay-record-body">
          <div className="pay-record-row"><span className="prv-lbl">วันที่ชำระ</span><span className="prv-val mono">{record.paymentDate || '—'}</span></div>
          <div className="pay-record-row"><span className="prv-lbl">เวลาชำระ</span><span className="prv-val mono">{timeStr}</span></div>

          {/* วิธีชำระ — editable before lock */}
          <div className="pay-record-row">
            <span className="prv-lbl">วิธีชำระ</span>
            {!effectiveLocked ? (
              <select className="prv-edit-select"
                value={editChannel}
                onChange={(e) => { setEditChannel(e.target.value); setDirty(true); }}>
                {channels.map((c) => <option key={c} value={c}>{c === 'Key-in' ? 'KEY-IN' : c}</option>)}
              </select>
            ) : (
              <span className="prv-val">{record.channel === 'Key-in' ? 'KEY-IN' : (record.channel || '—')}</span>
            )}
          </div>

          {/* เวลา QR — editable when channel is QR */}
          {(editChannel === 'QR' || record.qrTime) && (
            <div className="pay-record-row">
              <span className="prv-lbl">เวลา QR</span>
              {!effectiveLocked && editChannel === 'QR' ? (
                <input className="prv-edit-input mono" type="time"
                  value={editQrTime}
                  onChange={(e) => { setEditQrTime(e.target.value); setDirty(true); }} />
              ) : (
                <span className="prv-val mono">{record.qrTime || '—'}</span>
              )}
            </div>
          )}

          {/* ยอดรับ — editable before lock, blocked for multi-alloc */}
          <div className="pay-record-row pay-record-amount">
            <span className="prv-lbl">ยอดรับ</span>
            {!effectiveLocked && !isMultiAlloc ? (
              <input className="prv-edit-input mono" type="number" min="0" step="1"
                value={editAmt}
                onChange={(e) => { setEditAmt(e.target.value); setDirty(true); }} />
            ) : (
              <span className="prv-val mono">{fmtMoney(record.amount)}</span>
            )}
          </div>

          {isMultiAlloc && !effectiveLocked && (
            <div className="prv-note">แก้ไขยอดไม่ได้ (ชำระข้ามคืน) — แก้ไขวิธีชำระได้</div>
          )}

          {allocations.length > 1 && (
            <div className="pay-record-allocations">
              <div className="prv-alloc-title">แบ่งชำระข้ามคืน</div>
              {allocations.map((a, i) => (
                <div className="pay-record-row prv-alloc-line" key={a.reservationId || i}>
                  <span className="prv-lbl">รายการที่ {i + 1}</span>
                  <span className="prv-val mono">{fmtMoney(a.amount)}</span>
                </div>
              ))}
            </div>
          )}

          {record.createdBy && <div className="pay-record-row"><span className="prv-lbl">บันทึกโดย</span><span className="prv-val">{record.createdBy}</span></div>}
        </div>

        <div className="popup-foot">
          {!effectiveLocked && !confirmVoid && (
            <button className="pbtn del-ghost" style={{ marginRight: 'auto' }}
              onClick={() => setConfirmVoid(true)}>
              ลบรายการนี้
            </button>
          )}
          {!effectiveLocked && confirmVoid && (
            <>
              <span className="prv-void-confirm" style={{ marginRight: 'auto', fontSize: 12, color: 'var(--error-red)' }}>
                ลบรายการรับเงินนี้?<br/>
                <span style={{ color: 'var(--text-secondary)', fontWeight: 400 }}>ระบบจะคำนวณยอดค้างใหม่ และสามารถรับเงินใหม่ได้อีกครั้ง</span>
              </span>
              <button className="pbtn ghost" onClick={() => setConfirmVoid(false)}>ไม่</button>
              <button className="pbtn primary red" onClick={handleVoid}>ยืนยันลบ</button>
            </>
          )}
          {!confirmVoid && (
            <>
              <button className="pbtn ghost" onClick={onClose}>ปิด</button>
              {!effectiveLocked && dirty && (
                <button className="pbtn primary" onClick={handleSave}>บันทึกการแก้ไข</button>
              )}
            </>
          )}
        </div>
      </div>
    </Scrim>
  );
}

function MoveRoomForm({ booking, rooms, occupiedRooms, maintainedRooms, onClose, onSave }) {
  const choices = rooms.filter((room) => String(room) !== String(booking.room) && !occupiedRooms.includes(String(room)) && !maintainedRooms.includes(String(room)));
  const [room, setRoom] = useStateP('');
  const [reason, setReason] = useStateP('');
  const [note, setNote] = useStateP('');
  const reasonOptions = [
    { value: 'broken',  label: 'ห้องเสีย / แจ้งซ่อม' },
    { value: 'general', label: 'อื่น ๆ / ย้ายห้องทั่วไป' },
  ];
  const reasonLabel = (reasonOptions.find((o) => o.value === reason) || {}).label || reason;
  const submit = () => {
    if (!room) { alert('กรุณาเลือกห้องใหม่'); return; }
    if (!reason) { alert('กรุณาเลือกเหตุผลการย้ายห้อง'); return; }
    if (!confirm(`ย้ายจากห้อง ${booking.room} ไปห้อง ${room}\nเหตุผล: ${reasonLabel}\nโดยคง Booking, Payment และ INV เดิม?`)) return;
    onSave(room, reason, note);
  };
  return <Scrim onClose={onClose}><div className="popup compact-popup" onMouseDown={(e) => e.stopPropagation()}>
    <div className="popup-head"><h3>ย้ายห้อง · ห้อง {booking.room}</h3><button className="popup-x" aria-label="ปิด" title="ปิด" onClick={onClose}>×</button></div>
    <div className="form-grid">
      <div className="fld"><label>ห้องเดิม</label><input className="mono" value={booking.room} disabled /></div>
      <div className="fld"><label>ห้องใหม่</label><select className="mono" value={room} onChange={(e) => setRoom(e.target.value)}><option value="">เลือกห้อง</option>{choices.map((r) => <option key={r}>{r}</option>)}</select></div>
      <div className="fld full"><label>เหตุผล <span style={{color:'var(--error-red)'}}>*</span></label>
        <select value={reason} onChange={(e) => setReason(e.target.value)}>
          <option value="">-- เลือกเหตุผล --</option>
          {reasonOptions.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
        </select>
      </div>
      <div className="fld full"><label>หมายเหตุเพิ่มเติม</label><input value={note} onChange={(e) => setNote(e.target.value)} placeholder="ระบุรายละเอียด (ถ้ามี)" /></div>
    </div>
    <div className="popup-foot"><button className="pbtn ghost" onClick={onClose}>ยกเลิก</button><button className="pbtn primary" disabled={!room || !reason} onClick={submit}>ยืนยันย้ายห้อง</button></div>
  </div></Scrim>;
}

function StayoverForm({ booking, side, onClose, onSave }) {
  const start = toISO(booking.co);
  const [co, setCo] = useStateP(addDaysFromISO(start, 1));
  const [paidToday, setPaidToday] = useStateP('');
  const [pendingAmount, setPendingAmount] = useStateP('');
  const [channel, setChannel] = useStateP('');
  const [qrTime, setQrTime] = useStateP('');
  const [reason, setReason] = useStateP('');
  const [free, setFree] = useStateP(false);
  const paymentMethods = side === 'inv' ? ['Cash','QR','EDC','Key-in'] : ['Cash','QR','BBL','AGP'];
  const paid = Number(paidToday || 0);
  const outstanding = Number(pendingAmount || 0);
  const invalidPaid = !Number.isFinite(paid) || paid < 0;
  const invalidPending = !Number.isFinite(outstanding) || outstanding < 0;
  const invalid = !(new Date(start) < new Date(co))
    || invalidPaid || invalidPending
    || (!free && paid === 0 && outstanding === 0)
    || (!free && paid > 0 && !channel)
    || (!free && side === 'noinv' && paid > 0 && channel === 'QR' && !qrTime.trim())
    || (free && !reason.trim());
  const toggleFree = () => { setFree((v) => !v); setPaidToday(''); setPendingAmount(''); setChannel(''); setQrTime(''); };
  const submit = () => {
    if (invalid) return;
    onSave({ ci: toDisplay(start), co: toDisplay(co), free, paidAmount: free ? 0 : paid, outstanding: free ? 0 : outstanding, channel: free ? '' : channel, qrTime: free ? '' : qrTime, reason });
  };
  return <Scrim onClose={onClose}><div className="popup stayover-popup" onMouseDown={(e) => e.stopPropagation()}>
    <div className="popup-head"><h3>อยู่ต่อ · ห้อง {booking.room}</h3><button className="popup-x" aria-label="ปิด" title="ปิด" onClick={onClose}>×</button></div>
    <div className="form-grid stayover-grid">
      <div className="fld"><input aria-label="เริ่มจาก C/O เดิม" className="mono" type="date" value={start} disabled /></div>
      <div className={'fld' + (!(new Date(start) < new Date(co)) ? ' field-invalid' : '')}><input aria-label="C/O ใหม่" className="mono" type="date" value={co} onChange={(e) => setCo(e.target.value)} /></div>
      <div className="fld full stayover-payment-row">
      <div className={'fld stayover-pay-method' + (!free && paid > 0 && !channel ? ' field-invalid' : '')}>
        <select disabled={free} value={channel} onChange={(e) => { setChannel(e.target.value); if (e.target.value !== 'QR') setQrTime(''); }}><option value="">วิธีชำระ</option>{paymentMethods.map((x) => <option key={x}>{x}</option>)}</select>
        {!free && side === 'noinv' && paid > 0 && channel === 'QR' && <input className="mono" value={qrTime} onChange={(e) => setQrTime(e.target.value)} placeholder="เวลา QR" />}
      </div>
      <div className={'fld' + (invalidPaid || (!free && paid === 0 && outstanding === 0) ? ' field-invalid' : '')}><input className="mono" type="number" min="0" disabled={free || !channel} value={paidToday} onChange={(e) => setPaidToday(e.target.value)} placeholder="ยอดชำระ" /></div>
      <div className={'fld' + (invalidPending || (!free && paid === 0 && outstanding === 0) ? ' field-invalid' : '')}><input className="mono due-fld" type="number" min="0" disabled={free} value={pendingAmount} onChange={(e) => setPendingAmount(e.target.value)} placeholder="ยอดค้าง" /></div>
      </div>
      <div className="fld full stayover-note-row"><input value={reason} onChange={(e) => setReason(e.target.value)} placeholder={free ? 'เหตุผลสำหรับอยู่ต่อฟรี' : 'หมายเหตุ'} />
        <button type="button" className={'stayover-free-btn' + (free ? ' on' : '')} onClick={toggleFree}>อยู่ต่อฟรี</button>
      </div>
    </div>
    <div className="popup-foot"><button className="pbtn ghost" onClick={onClose}>ยกเลิก</button><button className="pbtn primary green" disabled={invalid} onClick={submit}>บันทึกอยู่ต่อ</button></div>
  </div></Scrim>;
}

/* ---------- Expense form ---------- */
function ExpenseForm({ edit, onClose, onSave, onDelete }) {
  const [f, setF] = useStateP(edit || { cat: H.EXPENSE_CATS[0], amount: '', detail: '', note: '' });
  const set = (k, v) => setF((s) => ({ ...s, [k]: v }));
  const amount = Number(f.amount);
  const amountInvalid = !Number.isFinite(amount) || amount <= 0;
  return (
    <Scrim onClose={onClose}>
      <div className="popup" style={{ width: 'var(--popup-w-md)' }} onMouseDown={(e) => e.stopPropagation()}>
        <div className="popup-head"><h3>{edit ? 'แก้ไขรายจ่าย' : 'เพิ่มรายจ่าย'}</h3><button className="popup-x" aria-label="ปิด" title="ปิด" onClick={onClose}>×</button></div>
        {edit && <div className="edit-banner">แก้ไขรายการเดิม บันทึกจะอัปเดตรายการนี้เท่านั้น</div>}
        <div className="form-grid">
          <div className="fld"><label>หมวด</label>
            <select value={f.cat} onChange={(e) => set('cat', e.target.value)}>
              {H.EXPENSE_CATS.map((c) => <option key={c}>{c}</option>)}
            </select></div>
          <div className="fld"><label>จำนวนเงิน</label><input className="mono" type="number" min="0" value={f.amount} onChange={(e) => set('amount', e.target.value)} placeholder="0" />
            {amountInvalid && <span className="fld-error">กรุณากรอกจำนวนเงินมากกว่า 0</span>}</div>
          <div className="fld full"><label>รายละเอียด</label><input value={f.detail} onChange={(e) => set('detail', e.target.value)} placeholder="รายละเอียด" /></div>
        </div>
        <div className="popup-foot">
          {onDelete && <button className="pbtn del-ghost" style={{ marginRight: 'auto' }} onClick={onDelete}>ลบรายการ</button>}
          <button className="pbtn ghost" onClick={onClose}>ยกเลิก</button>
          <button className="pbtn primary red" disabled={amountInvalid} onClick={() => onSave({ ...f, amount })}>{edit ? 'อัปเดต' : 'บันทึก'}</button>
        </div>
      </div>
    </Scrim>
  );
}

/* ---------- Room picker ---------- */
// multiSelect=false (default): single-click returns one room and closes.
// multiSelect=true (used when creating a new Payment Group): shows
//   selected-state buttons, confirm bar, returns rooms[] via onPickMulti.
function RoomPicker({ onClose, onPick, onPickMulti, usedRooms, multiSelect, initialSelected }) {
  const [selected, setSelected] = useStateP(initialSelected || []);
  const toggle = (rm) => setSelected((prev) =>
    prev.includes(rm) ? prev.filter((r) => r !== rm) : [...prev, rm]);
  return (
    <Scrim onClose={onClose}>
      <div className="popup room-picker-popup" onMouseDown={(e) => e.stopPropagation()}>
        <div className="popup-head">
          <h3>{multiSelect ? 'เลือกห้องสำหรับกลุ่ม' : 'เลือกห้อง'}</h3>
          <button className="popup-x" aria-label="ปิด" title="ปิด" onClick={onClose}>×</button>
        </div>
        {multiSelect && (
          <div className="rp-ms-hint">
            คลิกเลือกหลายห้อง · ห้องแรกที่เลือกเป็น Primary · กด ยืนยัน เมื่อครบ
          </div>
        )}
        <div className="rp-body">
          {Object.entries(H.ROOMS_BY_FLOOR).map(([floor, rooms]) => (
            <div className="rp-floor" key={floor}>
              <h4>{floor}</h4>
              <div className="rp-grid">
                {rooms.map((rm) => {
                  const fix = H.BROKEN.includes(rm);
                  const guest = H.SOLD.includes(rm);
                  const dup = usedRooms.includes(rm);
                  const sel = multiSelect && selected.includes(rm);
                  const isPrimary = multiSelect && selected.length > 0 && selected[0] === rm;
                  const cls = 'rm'
                    + (fix ? ' fix' : guest ? ' guest' : '')
                    + (sel ? ' rp-sel' : '')
                    + (isPrimary ? ' rp-primary' : '');
                  if (multiSelect) {
                    return (
                      <button key={rm} style={{ position: 'relative' }} className={cls} disabled={fix}
                        onClick={() => !fix && toggle(rm)}>
                        {rm}
                        {dup && !fix && !sel && <span className="warn">!</span>}
                        {isPrimary && <span className="rp-p-badge">P</span>}
                      </button>
                    );
                  }
                  return (
                    <button key={rm} className={cls} disabled={fix} onClick={() => !fix && onPick(rm)}>
                      {rm}{dup && !fix && <span className="warn">!</span>}
                    </button>
                  );
                })}
              </div>
            </div>
          ))}
        </div>
        {multiSelect && (
          <div className="rp-confirm-bar">
            <span className="rp-sel-count">
              {selected.length === 0
                ? 'ยังไม่ได้เลือกห้อง'
                : (selected.length === 1
                    ? ('Primary: ห้อง ' + selected[0])
                    : ('Primary: ' + selected[0] + ' · Member: ' + selected.slice(1).join(', ')))}
            </span>
            <button className="pbtn primary" disabled={selected.length === 0}
              onClick={() => selected.length > 0 && onPickMulti(selected)}>
              ยืนยัน ({selected.length} ห้อง)
            </button>
          </div>
        )}
      </div>
    </Scrim>
  );
}

/* ---------- Delete confirm ---------- */
function DeleteConfirm({ onClose, onConfirm }) {
  return (
    <Scrim onClose={onClose}>
      <div className="popup del-pop" onMouseDown={(e) => e.stopPropagation()}>
        <div className="ic">ลบ</div>
        <h3>ลบรายการนี้?</h3>
        <p>รายการจะถูกยกเลิก (void) — ประวัติยังคงอยู่ใน Audit Log</p>
        <div style={{ display: 'flex', gap: 10, justifyContent: 'center' }}>
          <button className="pbtn ghost" onClick={onClose}>ยกเลิก</button>
          <button className="pbtn danger" onClick={onConfirm}>ลบ</button>
        </div>
      </div>
    </Scrim>
  );
}

/* ---------- Lock confirm ---------- */
function LockConfirm({ start, end, onClose, onConfirm }) {
  return (
    <Scrim onClose={onClose}>
      <div className="popup del-pop" onMouseDown={(e) => e.stopPropagation()}>
        <div className="ic" style={{ background: 'rgba(13,13,11,.1)', color: 'var(--fg1)' }}>ปิดรอบ</div>
        <h3>ปิดรอบวันนี้?</h3>
        <p>เวลา <b className="mono-c">{start}</b> – <b className="mono-c">{end}</b><br />หลังปิดรอบ พนักงานจะแก้ไขไม่ได้</p>
        <div style={{ display: 'flex', gap: 10, justifyContent: 'center' }}>
          <button className="pbtn ghost" onClick={onClose}>ยกเลิก</button>
          <button className="pbtn primary green" onClick={onConfirm}>ปิดรอบ</button>
        </div>
      </div>
    </Scrim>
  );
}

/* ---------- Unlock confirm (เจ้าของเท่านั้น) ---------- */
function UnlockConfirm({ date, onClose, onConfirm }) {
  return (
    <Scrim onClose={onClose}>
      <div className="popup del-pop" onMouseDown={(e) => e.stopPropagation()}>
        <div className="ic" style={{ background: 'rgba(13,13,11,.1)', color: 'var(--fg1)' }}>ปลดล็อก</div>
        <h3>ปลดล็อกวันนี้?</h3>
        <p>วันที่ <b className="mono-c">{date}</b><br />หลังปลดล็อก จะแก้ไข/เพิ่มรายการได้อีกครั้ง</p>
        <div style={{ display: 'flex', gap: 10, justifyContent: 'center' }}>
          <button className="pbtn ghost" onClick={onClose}>ยกเลิก</button>
          <button className="pbtn primary" onClick={onConfirm}>ปลดล็อก</button>
        </div>
      </div>
    </Scrim>
  );
}

function RoomActionPopup({ room, onClose, onStay, onStatus }) {
  return (
    <Scrim onClose={onClose}>
      <div className="popup room-action-pop" onMouseDown={(e) => e.stopPropagation()}>
        <button className="popup-x room-action-x" aria-label="ปิด" title="ปิด" onClick={onClose}>×</button>
        <div className="room-action-number">{room}</div>
        <div className="room-action-cards">
          <button className="room-action-card room-action-card-blue" onClick={onStay}>สร้างรายการ</button>
          <button className="room-action-card room-action-card-red" onClick={onStatus}>ฟรี/เสีย</button>
        </div>
      </div>
    </Scrim>
  );
}

/* ---------- Close-for-maintenance form (ปิดปรับปรุง) ---------- */
function MaintenanceForm({ room, edit, shiftDate, onClose, onSave, onReopen }) {
  const brokenReasons = ['ปัญหาแอร์', 'ปัญหาทีวี', 'ปัญหาระบบน้ำ', 'ทำความสะอาด / ห้องยังไม่พร้อม', 'ซ่อมบำรุงทั่วไป', 'อื่นๆ'];
  const [type, setType] = useStateP((edit && edit.type) || 'broken');
  const [reason, setReason] = useStateP((edit && edit.reason) || brokenReasons[0]);
  const [note, setNote] = useStateP((edit && edit.note) || '');
  const [startDate, setStartDate] = useStateP(toISO((edit && edit.startDate) || shiftDate || todayISO()));
  const initialStart = toISO((edit && edit.startDate) || shiftDate || todayISO());
  const [endDate, setEndDate] = useStateP(toISO((edit && edit.endDate) || addDaysFromISO(initialStart, 1)));
  const invalid = !(new Date(startDate) < new Date(endDate));
  const pick = (t) => { if (t === type) return; setType(t); setReason(t === 'broken' ? brokenReasons[0] : ''); };
  return (
    <Scrim onClose={onClose}>
      <div className="popup" style={{ width: 'var(--popup-w-md)' }} onMouseDown={(e) => e.stopPropagation()}>
        <div className="popup-head">
          <h3>ห้อง {room} · สถานะห้อง</h3>
          <button className="popup-x" aria-label="ปิด" title="ปิด" onClick={onClose}>×</button>
        </div>
        <div className="form-grid" style={{ gridTemplateColumns: '1fr' }}>
          <div className="maint-date-pair">
            <div className="fld"><label>วันที่เริ่ม</label><input className="mono" type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} /></div>
            <div className="fld"><label>วันที่สิ้นสุด</label><input className="mono" type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} /></div>
          </div>
          <div className="fld full"><label>ประเภท</label>
            {edit
              ? <div className="maint-type-readonly">{type === 'broken' ? 'เสีย (แจ้งซ่อม)' : 'ฟรี'}</div>
              : <div className="side-toggle maint-type-toggle">
                  <button className={type === 'broken' ? 'on' : ''} onClick={() => pick('broken')}>เสีย (แจ้งซ่อม)</button>
                  <button className={type === 'free' ? 'on' : ''} onClick={() => pick('free')}>ฟรี</button>
                </div>}
          </div>
          <div className={'fld full' + (type !== 'broken' ? ' maint-field-hidden' : '')}><label>สาเหตุ</label>
            <select value={reason} onChange={(e) => setReason(e.target.value)} tabIndex={type !== 'broken' ? -1 : 0}>
              {brokenReasons.map((r) => <option key={r}>{r}</option>)}
            </select></div>
          <div className="fld full"><label>รายละเอียดเพิ่มเติม</label>
            <input value={note} onChange={(e) => setNote(e.target.value)}
              placeholder={type === 'broken' ? 'เช่น คอมเพรสเซอร์เสีย, รอช่าง…' : 'ระบุรายละเอียด (ถ้ามี)'} /></div>
          <div className="maint-rec">บันทึกประวัติผู้ทำ เวลา และสาเหตุไว้ใน Audit Log</div>
        </div>
        <div className="popup-foot">
          {edit && <button className="pbtn primary green" style={{ marginRight: 'auto' }} onClick={onReopen}>นำกลับมาขาย</button>}
          <button className="pbtn ghost" onClick={onClose}>ยกเลิก</button>
          <button className="pbtn primary" disabled={invalid} onClick={() => onSave({ type, reason, note, startDate: toDisplay(startDate), endDate: toDisplay(endDate) })}>{edit ? 'อัปเดต' : 'บันทึก'}</button>
        </div>
      </div>
    </Scrim>
  );
}

Object.assign(window, { IncomeForm, ExpenseForm, RoomPicker, DeleteConfirm, LockConfirm, UnlockConfirm, RoomActionPopup, MaintenanceForm, MoveRoomForm, StayoverForm });
