// OP Central — Procurement screens: RFQ, Vendor PO, GRN, 3-Way Match

// ===== Auto comparison engine =====
// For one project (SO): compare the chosen vendor per item against the cheapest
// available across all vendors, and roll up revenue / spend / savings.
function projectComparison(state, soId, getProduct) {
  const so = state.sales_orders.find(x => x.id === soId);
  if (!so) return null;
  const sourcing = soSourcing(state, soId);
  const pos = state.vendor_pos.filter(p => p.so_id === soId);
  const req = soReqComponents(so);
  const picks = (sourcing && sourcing.picks) || {};
  const sprices = (sourcing && sourcing.prices) || {};
  const priceFor = (vid, p) => (sprices[p.id] && sprices[p.id][vid] != null)
    ? sprices[p.id][vid]
    : (window.vendorUnitPrice ? window.vendorUnitPrice(vid, p) : (p.buy || 0));
  const poLineVendor = (pid) => {
    for (const po of pos) { const it = (po.items || []).find(x => x.product_id === pid); if (it) return { vid: po.vendor_id, rate: it.rate }; }
    return null;
  };
  const rows = Object.entries(req).map(([pid, qty]) => {
    const p = getProduct(pid) || { id: pid, name: pid, code: pid, buy: 0 };
    const all = state.vendors.map(v => ({ v, price: priceFor(v.id, p) })).sort((a, b) => a.price - b.price);
    const cheapest = all[0] || null;
    const fromPO = poLineVendor(pid);
    const chosenVid = fromPO ? fromPO.vid : (picks[pid] || (cheapest && cheapest.v.id));
    const chosenRate = fromPO ? fromPO.rate : priceFor(chosenVid, p);
    const cheapestRate = cheapest ? cheapest.price : chosenRate;
    return { pid, p, qty, all, chosenVid, chosenRate, cheapestVid: cheapest && cheapest.v.id, cheapestRate, lineSpend: chosenRate * qty, lineBest: cheapestRate * qty, saving: (chosenRate - cheapestRate) * qty };
  });
  const spend = rows.reduce((s, r) => s + r.lineSpend, 0);
  const best = rows.reduce((s, r) => s + r.lineBest, 0);
  const indicative = (so.lines || []).reduce((s, l) => s + (l.bundle_qty || 0) * (l.unit_price || 0), 0);
  const revenue = (sourcing && sourcing.our_price) ? Number(sourcing.our_price) : indicative;
  return { so, sourcing, pos, rows, spend, best, potentialSaving: spend - best, revenue, poTotal: pos.reduce((s, p) => s + (p.amount || 0), 0), marginPct: revenue ? ((revenue - spend) / revenue) * 100 : 0 };
}
window.projectComparison = projectComparison;

// ===== RFQ / Vendor Comparison page (auto engine) =====
function RFQList() {
  const { state, navigate, getProduct, getVendor, getCustomer } = useStore();

  // Auto-fetch every project that has sourcing and/or vendor POs.
  const ids = new Set();
  (state.sourcings || []).forEach(s => { if (s.converted_so_id) ids.add(s.converted_so_id); });
  state.vendor_pos.forEach(p => { if (p.so_id) ids.add(p.so_id); });
  const comps = [...ids].map(id => projectComparison(state, id, getProduct)).filter(Boolean);
  const totSpend = comps.reduce((s, c) => s + c.spend, 0);
  const totSaving = comps.reduce((s, c) => s + c.potentialSaving, 0);

  return (
    <div className="page">
      <div className="page-header">
        <div>
          <h1 className="page-title">RFQ / Vendor Comparison</h1>
          <div className="page-sub">Automatic cross-vendor comparison across all projects · chosen vs cheapest, mapped per item · live from real PO + sourcing data</div>
        </div>
      </div>

      <div className="mb-2" style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 10 }}>
        <div className="card"><div className="card-body" style={{ textAlign: 'center' }}><div className="tiny muted">Projects compared</div><div style={{ fontSize: 20, fontWeight: 700 }}>{comps.length}</div></div></div>
        <div className="card"><div className="card-body" style={{ textAlign: 'center' }}><div className="tiny muted">Total vendor spend</div><div style={{ fontSize: 20, fontWeight: 700 }} className="mono">{inr(totSpend)}</div></div></div>
        <div className="card" style={{ borderColor: totSaving > 0 ? 'oklch(0.85 0.09 75)' : undefined }}><div className="card-body" style={{ textAlign: 'center', background: totSaving > 0 ? 'var(--warning-bg)' : undefined }}><div className="tiny muted">Potential extra savings</div><div style={{ fontSize: 20, fontWeight: 700, color: totSaving > 0 ? 'var(--warning)' : 'var(--success)' }} className="mono">{inr(totSaving)}</div></div></div>
      </div>

      {comps.length === 0 ? (
        <div className="card"><div className="empty">
          <div className="empty-title">Nothing to compare yet</div>
          Once inquiries are converted to SOs and vendors are chosen, every project's vendor comparison appears here automatically.
        </div></div>
      ) : comps.map(c => {
        const cust = getCustomer(c.so.customer_id);
        return (
          <div className="card mb-2" key={c.so.id}>
            <div className="card-header">
              <div>
                <h3 className="card-title"><span className="mono" onClick={() => navigate(`sales-orders/${c.so.id}`)} style={{ cursor: 'pointer' }}>{c.so.so_no}</span> — {cust ? cust.name : ''}</h3>
                <div className="tiny muted">revenue {inr(c.revenue)} · vendor spend {inr(c.spend)} · margin <strong style={{ color: c.marginPct >= 0 ? 'var(--success)' : 'var(--danger)' }}>{c.marginPct >= 0 ? '+' : ''}{c.marginPct.toFixed(1)}%</strong>{c.potentialSaving > 0 ? <> · <span style={{ color: 'var(--warning)' }}>save {inr(c.potentialSaving)} more if all-cheapest</span></> : ' · already optimal'}</div>
              </div>
              <span className="badge dot">{c.pos.length} PO(s)</span>
            </div>
            <div className="card-body flush">
              <table className="t">
                <thead><tr><th>Item</th><th className="num">Qty</th><th>Chosen vendor</th><th className="num">Rate</th><th>Cheapest</th><th className="num">Best rate</th><th className="num">Δ / line</th></tr></thead>
                <tbody>
                  {c.rows.map(r => {
                    const cv = getVendor(r.chosenVid); const bv = getVendor(r.cheapestVid);
                    const optimal = r.chosenVid === r.cheapestVid || r.saving <= 0;
                    return (
                      <tr key={r.pid}>
                        <td>{r.p.name}<div className="tiny muted mono">{r.p.code}</div></td>
                        <td className="num">{r.qty}</td>
                        <td>{cv ? cv.name : '—'}</td>
                        <td className="num mono">{inr(r.chosenRate)}</td>
                        <td>{optimal ? <span className="muted">— same</span> : (bv ? bv.name : '—')}</td>
                        <td className="num mono">{inr(r.cheapestRate)}</td>
                        <td className="num">{optimal ? <span style={{ color: 'var(--success)' }}>best</span> : <span style={{ color: 'var(--warning)', fontWeight: 600 }}>+{inr(r.saving)}</span>}</td>
                      </tr>
                    );
                  })}
                </tbody>
                <tfoot><tr><td colSpan="3" className="right small">Totals</td><td className="num mono">{inr(c.spend)}</td><td></td><td className="num mono">{inr(c.best)}</td><td className="num">{c.potentialSaving > 0 ? <strong style={{ color: 'var(--warning)' }}>+{inr(c.potentialSaving)}</strong> : <span style={{ color: 'var(--success)' }}>optimal</span>}</td></tr></tfoot>
              </table>
            </div>
          </div>
        );
      })}
    </div>
  );
}

// Our (sell) value of a PO's items — for per-vendor margin.
function poOurValue(po, getProduct) {
  return (po.items || []).reduce((s, it) => { const p = getProduct(it.product_id); return s + (p ? (p.sell || 0) : 0) * (it.qty || 0); }, 0);
}

// Same, but just one SO's own share of a PO that's shared across several —
// so a combined PO's value isn't counted in full on every linked SO's own
// margin card. Identical to poOurValue whenever the PO isn't actually shared.
function poOurValueForSO(po, soId, getProduct) {
  return (po.items || []).reduce((s, it) => { const p = getProduct(it.product_id); return s + (p ? (p.sell || 0) : 0) * poLineSoQty(po, it.product_id, soId); }, 0);
}

// Resolve an MD review of a PO. If a vendor-change is pending, approve swaps the
// vendor/items/amount in automatically; reject discards it (old vendor kept). For
// a plain over-threshold PO: approve → Issued, reject → Rejected.
function applyPOReview(po, approve) {
  if (po.pending_change) {
    if (approve) { const c = po.pending_change; return { ...po, vendor_id: c.vendor_id, items: c.items, amount: c.amount, status: 'Issued', pending_change: null }; }
    return { ...po, status: 'Issued', pending_change: null };
  }
  return { ...po, status: approve ? 'Issued' : 'Rejected' };
}
window.applyPOReview = applyPOReview;

// Purchase proposes switching a PO to a CHEAPER vendor → goes to MD for approval.
function ChangeVendorModal({ po, onClose }) {
  const { state, mutate, getVendor, getProduct, currentUser } = useStore();
  const toast = useToast();
  const sourcing = window.soSourcing ? window.soSourcing(state, po.so_id) : null;
  const candIds = ((sourcing && (sourcing.quote_vendors || []).length) ? sourcing.quote_vendors : state.vendors.map(v => v.id)).filter(vid => vid !== po.vendor_id);
  const rateOf = (vid, p) => (sourcing && sourcing.prices && sourcing.prices[p.id] && sourcing.prices[p.id][vid] != null) ? Number(sourcing.prices[p.id][vid]) : (window.vendorUnitPrice ? window.vendorUnitPrice(vid, p) : (p ? (p.buy || 0) : 0));
  const [vid, setVid] = React.useState(candIds[0] || '');
  const [rates, setRates] = React.useState({});
  React.useEffect(() => { const r = {}; (po.items || []).forEach(it => { const p = getProduct(it.product_id); r[it.product_id] = vid ? rateOf(vid, p) : it.rate; }); setRates(r); }, [vid]);
  const newAmount = (po.items || []).reduce((s, it) => s + (it.qty || 0) * (Number(rates[it.product_id]) || 0), 0);
  const cheaper = newAmount > 0 && newAmount < po.amount;

  const submit = () => {
    if (!vid) { toast('Pick a new vendor'); return; }
    if (!cheaper) { toast('The new vendor total must be LESS than the current PO'); return; }
    const items = (po.items || []).map(it => ({ product_id: it.product_id, qty: it.qty, rate: Number(rates[it.product_id]) || 0 }));
    const change = { vendor_id: vid, items, amount: Math.round(newAmount), old_vendor_id: po.vendor_id, old_amount: po.amount, requested_by: currentUser || null, date: TODAY };
    mutate(s => ({ ...s, vendor_pos: s.vendor_pos.map(p => p.id === po.id ? { ...p, pending_change: change, status: 'Pending MD Approval' } : p), notifications: [{ id: 'n-vchg-' + Date.now(), kind: 'po', text: `${po.po_no}: vendor change → ${getVendor(vid)?.name} (${inrK(newAmount)} < ${inrK(po.amount)}) — awaiting MD approval`, date: TODAY, read: false, role: 'Managing Director' }, ...s.notifications] }), { action: 'vendor-change', entity: 'VendorPO', entity_id: po.id });
    toast('Vendor change sent to MD for approval', 'success');
    onClose();
  };

  return (
    <Modal title={`Change vendor — ${po.po_no}`} onClose={onClose} size="lg" footer={<><button className="btn" onClick={onClose}>Cancel</button><button className="btn btn-primary" disabled={!cheaper} onClick={submit}>Send to MD · {inr(newAmount)}</button></>}>
      {candIds.length === 0 ? (
        <div className="empty">No alternative vendors available for this order.</div>
      ) : (
        <>
          <div className="field-row mb-2">
            <div className="field"><label className="field-label">Current vendor</label><div className="input mono" style={{ background: 'var(--bg-subtle)' }}>{getVendor(po.vendor_id)?.name} · {inr(po.amount)}</div></div>
            <div className="field"><label className="field-label">New vendor *</label><select className="select" value={vid} onChange={e => setVid(e.target.value)}>{candIds.map(id => { const v = getVendor(id); return <option key={id} value={id}>{v ? v.name : id}</option>; })}</select></div>
          </div>
          <table className="t">
            <thead><tr><th>Item</th><th className="num">Qty</th><th className="num">Old rate</th><th className="num">New rate ₹</th><th className="num">Line</th></tr></thead>
            <tbody>
              {(po.items || []).map(it => { const p = getProduct(it.product_id) || { name: it.product_id }; return (
                <tr key={it.product_id}>
                  <td>{p.name}</td><td className="num">{qty(it.qty)}</td><td className="num small muted">{inr(it.rate)}</td>
                  <td className="num"><input type="number" min="0" className="input mono" value={rates[it.product_id] != null ? rates[it.product_id] : ''} onChange={e => setRates(r => ({ ...r, [it.product_id]: e.target.value }))} style={{ width: 90, textAlign: 'right', height: 26 }}/></td>
                  <td className="num mono">{inr((it.qty || 0) * (Number(rates[it.product_id]) || 0))}</td>
                </tr>
              ); })}
            </tbody>
            <tfoot><tr><td colSpan="4" className="right small">New total</td><td className="num mono" style={{ color: cheaper ? 'var(--success)' : 'var(--danger)' }}><strong>{inr(newAmount)}</strong></td></tr></tfoot>
          </table>
          <div className="mt-2" style={{ padding: 10, borderRadius: 4, background: cheaper ? 'var(--success-bg)' : 'var(--danger-bg)', fontSize: 12.5 }}>
            {cheaper ? `Saves ${inr(po.amount - newAmount)} vs the current vendor — allowed.` : `Must be lower than the current PO (${inr(po.amount)}). A more expensive switch isn't allowed.`}
          </div>
        </>
      )}
    </Modal>
  );
}
window.ChangeVendorModal = ChangeVendorModal;

// The write side of editing which SOs share an ALREADY-ISSUED line (Moment
// 3) — same so_alloc primitive CreateVendorPOModal/BulkVendorPOModal write
// at creation. Pure: builds the patched PO and says which SOs are newly
// linked (so the caller can advance/notify them); writes nothing itself.
function applyLineSoAlloc(po, productId, so_alloc) {
  const before = new Set(poLinkedSoIds(po));
  const items = (po.items || []).map(it => it.product_id === productId ? { ...it, so_alloc } : it);
  const amount = Math.round(items.reduce((s, it) => s + (Number(it.qty) || 0) * (Number(it.rate) || 0), 0));
  const linkedSoIds = poLinkedSoIds({ ...po, items });
  const newlyLinked = linkedSoIds.filter(id => !before.has(id));
  return { po: { ...po, items, amount }, linkedSoIds, newlyLinked };
}
window.applyLineSoAlloc = applyLineSoAlloc;

// Per-product on-PO / received totals for an SO, AND which specific PO(s)
// each total comes from — an SO routinely has more than one vendor PO
// (different vendors, batches, or a PO combined with another SO entirely),
// and a combined number with no way to see which PO it came from is exactly
// the guess-presented-as-fact "show where a number came from" rule exists
// to prevent. Only lists a PO against a product when it actually has
// quantity on/received for it — never a phantom zero entry. Pure read-only.
function soItemPOBreakdown(state, so, pos) {
  const receivedByProd = {}, onPOByProd = {};
  const onPOByProdPO = {}, receivedByProdPO = {};
  const grnsForPO = (poId) => (state.grns || []).filter(g => g.po_id === poId);
  pos.forEach(po => {
    (po.items || []).forEach(it => {
      const q = poLineSoQty(po, it.product_id, so.id);
      onPOByProd[it.product_id] = (onPOByProd[it.product_id] || 0) + q;
      if (q > 0.0001) (onPOByProdPO[it.product_id] = onPOByProdPO[it.product_id] || []).push({ po_id: po.id, po_no: po.po_no, qty: q });
    });
    const recvForThisPO = {};
    grnsForPO(po.id).forEach(g => (g.items || []).forEach(it => {
      const q = grnLineSoQty(it, po, so.id);
      receivedByProd[it.product_id] = (receivedByProd[it.product_id] || 0) + q;
      if (q > 0.0001) recvForThisPO[it.product_id] = (recvForThisPO[it.product_id] || 0) + q;
    }));
    Object.keys(recvForThisPO).forEach(pid => { (receivedByProdPO[pid] = receivedByProdPO[pid] || []).push({ po_id: po.id, po_no: po.po_no, qty: recvForThisPO[pid] }); });
  });
  return { onPOByProd, receivedByProd, onPOByProdPO, receivedByProdPO };
}
window.soItemPOBreakdown = soItemPOBreakdown;

// A quick, upfront, visual answer to "which SO(s) is this PO for, and how
// much of what" — shown once, before any line-item ticking, so Stores never
// discovers a PO is shared only after a per-line split editor appears deep
// in the form. Renders nothing for the overwhelming majority of POs, which
// still serve just one SO — zero visual change there.
function ComboPOSummary({ po, getSO, getProduct }) {
  const linked = poLinkedSoIds(po);
  if (linked.length <= 1) return null;
  const rows = [];
  (po.items || []).forEach(it => {
    const p = getProduct(it.product_id);
    linked.forEach(soId => {
      const q = poLineSoQty(po, it.product_id, soId);
      if (q > 0.0001) rows.push({ product: p ? p.name : it.product_id, so_id: soId, qty: q });
    });
  });
  return (
    <div className="card mb-2" style={{ borderLeft: '3px solid var(--accent)' }}>
      <div className="card-header">
        <h3 className="card-title"><Icon name="arrowLeftRight" size={14}/>This PO combines {linked.length} Sales Orders</h3>
        <span className="tiny muted">Each line's own split (below) decides exactly what this receipt gives each one</span>
      </div>
      <div className="card-body flush">
        <table className="t">
          <thead><tr><th>Item</th><th>Sales Order</th><th className="num">Ordered qty</th></tr></thead>
          <tbody>{rows.map((r, i) => { const so = getSO(r.so_id); return (
            <tr key={i}><td>{r.product}</td><td className="mono small">{so ? so.so_no : r.so_id}</td><td className="num">{qty(r.qty)}</td></tr>
          ); })}</tbody>
        </table>
      </div>
    </div>
  );
}
window.ComboPOSummary = ComboPOSummary;

// ===== Per-SO Vendor POs tab — every vendor mapped to ONE SO, with margins,
// item tracking (ordered → received → remaining) and payable status, in one
// place. Read-only consolidation; actions reuse the existing GRN / 3-way flow. =====
function SOVendorPOsTab({ so }) {
  const { state, navigate, mutate, getVendor, getProduct, soSubtotal, currentUser, getUser } = useStore();
  const toast = useToast();
  const [vF, setVF] = React.useState('');
  const [sF, setSF] = React.useState('');
  const [q, setQ] = React.useState('');
  const [showSplit, setShowSplit] = React.useState(false);
  const [receivePo, setReceivePo] = React.useState(null);
  const role = currentUser ? getUser(currentUser)?.role : '';
  const canProcure = ['Purchase', 'Project Manager', 'Stores', 'Org Admin'].includes(role);
  const canApprove = ['Managing Director', 'Org Admin'].includes(role);
  const sourcing = window.soSourcing ? window.soSourcing(state, so.id) : null;
  const setPoStatus = (po, status, msg) => {
    mutate(s => ({ ...s, vendor_pos: s.vendor_pos.map(p => p.id === po.id ? { ...p, status } : p), notifications: [{ id: 'n-poapp-' + Date.now(), kind: 'po', text: `${po.po_no} ${status === 'Issued' ? 'resumed' : status === 'Rejected' ? 'rejected by MD' : 'put on hold by MD'}`, date: TODAY, read: false, role: 'Purchase' }, ...s.notifications] }), { action: 'po-status', entity: 'VendorPO', entity_id: po.id });
    toast(msg, status === 'Rejected' ? '' : 'success');
  };
  const reviewPo = (po, approve) => {
    const changed = !!po.pending_change;
    mutate(s => ({ ...s, vendor_pos: s.vendor_pos.map(p => p.id === po.id ? applyPOReview(p, approve) : p), notifications: [{ id: 'n-porev-' + Date.now(), kind: 'po', text: `${po.po_no} ${approve ? (changed ? 'vendor change approved → ' + (getVendor(po.pending_change.vendor_id)?.name || '') : 'approved') : (changed ? 'vendor change declined' : 'rejected')} by MD`, date: TODAY, read: false, role: 'Purchase' }, ...s.notifications] }), { action: 'po-review', entity: 'VendorPO', entity_id: po.id });
    toast(approve ? `${po.po_no} approved` : `${po.po_no} ${changed ? 'change declined' : 'rejected'}`, approve ? 'success' : '');
  };

  const pos = state.vendor_pos.filter(p => poServesSO(p, so.id));
  const grnsFor = (poId) => state.grns.filter(g => g.po_id === poId);
  const viFor = (poId) => (state.vendor_invoices || []).filter(v => v.po_id === poId);
  const PoolPanel = window.VGAddFromPoolPanel;   // smart pool-suggestion panel (reused)

  // ===== Inline Vendor-PO editing (Purchase, after procurement starts) =====
  // Editable only while a PO hasn't been received and isn't awaiting MD — so no
  // GRN/e-Bill/3-way history is ever disturbed. Amount + MD gate recompute live.
  const [expanded, setExpanded] = React.useState({});
  const [addSel, setAddSel] = React.useState({});   // po.id -> product_id to add
  const [splitOpen, setSplitOpen] = React.useState({});   // "po.id|product_id" -> bool
  const mdThreshold = state.config.vendor_po_md_threshold != null ? state.config.vendor_po_md_threshold : 500000;
  const editablePO = (po) => canProcure && ['Issued', 'In Transit'].includes(po.status) && grnsFor(po.id).length === 0;
  // A PO that has received SOME of its lines can still have its OTHER,
  // unreceived lines edited — the lock is per-line, not per-PO. (Vendor
  // replacement above keeps the strict, zero-GRN-at-all gate: swapping
  // vendors after any stock has arrived would corrupt the GRN/invoice trail.)
  const poOpenForEdit = (po) => canProcure && ['Issued', 'In Transit', 'Partially Received'].includes(po.status);
  const lineReceived = (po, pid) => grnsFor(po.id).some(g => (g.items || []).some(it => it.product_id === pid && (Number(it.accepted) || 0) > 0));
  const lineEditable = (po, pid) => poOpenForEdit(po) && !lineReceived(po, pid);
  const recalc = (items) => items.reduce((s, it) => s + (Number(it.qty) || 0) * (Number(it.rate) || 0), 0);
  const savePOItems = (po, items, vendorId) => {
    const amount = Math.round(recalc(items));
    const needsMD = amount > mdThreshold;
    mutate(s => ({
      ...s,
      vendor_pos: s.vendor_pos.map(p => p.id === po.id ? { ...p, items, amount, status: needsMD ? 'Pending MD Approval' : po.status, ...(vendorId ? { vendor_id: vendorId } : {}) } : p),
      notifications: [{ id: 'n-poedit-' + Date.now(), kind: 'po', text: `${po.po_no} edited by Purchase · ${inrK(amount)}${needsMD ? ' · needs MD approval' : ''}`, date: TODAY, read: false, role: needsMD ? 'Managing Director' : 'Stores' }, ...s.notifications],
    }), { action: 'po-edit', entity: 'VendorPO', entity_id: po.id });
  };
  const setItemQty = (po, pid, qty) => savePOItems(po, (po.items || []).map(it => it.product_id === pid ? { ...it, qty: Math.max(0, Number(qty) || 0) } : it));
  const setItemRate = (po, pid, rate) => savePOItems(po, (po.items || []).map(it => it.product_id === pid ? { ...it, rate: Math.max(0, Number(rate) || 0) } : it));
  const removeItem = (po, pid) => savePOItems(po, (po.items || []).filter(it => it.product_id !== pid));
  const addItemToPO = (po) => { const pid = addSel[po.id]; if (!pid || (po.items || []).some(it => it.product_id === pid)) return; const p = getProduct(pid); const rate = window.vendorUnitPrice ? window.vendorUnitPrice(po.vendor_id, p) : (p ? p.buy || 0 : 0); savePOItems(po, [...(po.items || []), { product_id: pid, qty: 1, rate }]); setAddSel(m => ({ ...m, [po.id]: '' })); };
  const changePOVendor = (po, vid) => { if (!vid || vid === po.vendor_id) return; const items = (po.items || []).map(it => { const p = getProduct(it.product_id); const rate = window.vendorUnitPrice ? window.vendorUnitPrice(vid, p) : it.rate; return { ...it, rate }; }); savePOItems(po, items, vid); };
  // Editing which SOs share a line, after the PO already exists — same
  // so_alloc primitive CreateVendorPOModal writes at creation. Any newly
  // added SO gets notified and advances to 'Procurement Started', exactly
  // like today's single-SO "Create PO" already does.
  const setItemSoAlloc = (po, pid, so_alloc) => {
    const { po: patched, linkedSoIds, newlyLinked } = applyLineSoAlloc(po, pid, so_alloc);
    const needsMD = patched.amount > mdThreshold;
    mutate(s => ({
      ...s,
      vendor_pos: s.vendor_pos.map(p => p.id === po.id ? { ...p, items: patched.items, amount: patched.amount, status: needsMD ? 'Pending MD Approval' : po.status } : p),
      sales_orders: newlyLinked.length ? s.sales_orders.map(x => newlyLinked.includes(x.id) ? { ...x, status: soAdvanceStatus(x.status, 'Procurement Started') } : x) : s.sales_orders,
      notifications: [{ id: 'n-posplit-' + Date.now(), kind: 'po', text: `${po.po_no}: ${getProduct(pid)?.name || pid} now split across ${linkedSoIds.length} SO(s)${newlyLinked.length ? ' — ' + newlyLinked.map(id => (state.sales_orders.find(x => x.id === id) || {}).so_no || id).join(', ') + ' notified' : ''}${needsMD ? ' · needs MD approval' : ''}`, date: TODAY, read: false, role: needsMD ? 'Managing Director' : 'Stores' }, ...s.notifications],
    }), { action: 'po-split', entity: 'VendorPO', entity_id: po.id });
  };

  // Aggregate received / on-PO per product across this SO's POs, AND which
  // specific PO(s) each total comes from — see soItemPOBreakdown.
  const { onPOByProd, receivedByProd, onPOByProdPO, receivedByProdPO } = soItemPOBreakdown(state, so, pos);
  const required = soReqComponents(so);

  // A PO shared across SOs counts only this SO's own share here, never the
  // whole PO — identical to po.amount whenever the PO isn't actually shared.
  const poCostForSO = (po) => poLinkedSoIds(po).length > 1 ? poSoAmount(po, so.id) : (po.amount || 0);
  const poOurForSO = (po) => poLinkedSoIds(po).length > 1 ? poOurValueForSO(po, so.id, getProduct) : poOurValue(po, getProduct);
  // Actually committed so far — real money on real POs. Still the right
  // number for the per-PO table's own footer below (what's been placed),
  // but NOT the whole story for "what will this order earn": a component
  // never yet put on a PO used to read as zero cost here, so the grand
  // totals below used the SAME soProfit() the SO Overview's own profit
  // panel uses — committed + an estimate for what's still to buy — so the
  // two screens can never show a different margin for the same order.
  const vendorSpend = pos.reduce((s, p) => s + poCostForSO(p), 0);
  const profit = window.soProfit ? window.soProfit(state, so, getProduct) : null;
  const projectSell = profit ? profit.revenue : soSubtotal(so);
  const projectCost = profit ? profit.cost : vendorSpend;
  const projectEstimated = profit ? profit.estimated : 0;
  const projectMargin = profit ? profit.profit : projectSell - vendorSpend;
  const marginPct = profit && profit.margin != null ? profit.margin : (projectSell > 0 ? (projectMargin / projectSell) * 100 : 0);
  const payableBooked = pos.reduce((s, p) => s + viFor(p.id).filter(v => v.status === 'Booked').reduce((a, v) => a + (v.amount || 0), 0), 0);

  // Supply vs Implementation classification for the split view.
  const supplySet = new Set(); (so.lines || []).forEach(l => (l.components || []).forEach(c => supplySet.add(c.product_id)));
  const implReq = soImplReq(so); const implSet = new Set(Object.keys(implReq));
  const im = so.extra && so.extra.implementation;
  const typeOf = (pid) => (supplySet.has(pid) && implSet.has(pid)) ? 'Both' : implSet.has(pid) ? 'Impl' : 'Supply';
  const prodIds = Array.from(new Set([...Object.keys(required), ...Object.keys(onPOByProd), ...Object.keys(receivedByProd)]));
  const track = prodIds.map(pid => {
    const p = getProduct(pid);
    const req = required[pid] || 0, onPO = onPOByProd[pid] || 0, recv = receivedByProd[pid] || 0;
    return { pid, p, req, onPO, recv, remaining: Math.max(0, onPO - recv), shortfall: Math.max(0, req - onPO), type: typeOf(pid),
      onPOBreakdown: onPOByProdPO[pid] || [], recvBreakdown: receivedByProdPO[pid] || [] };
  }).filter(r => !q || (r.p && (r.p.name.toLowerCase().includes(q.toLowerCase()) || (r.p.code || '').toLowerCase().includes(q.toLowerCase()))));
  const shortfalls = track.filter(r => r.shortfall > 0).length;
  const awaiting = track.filter(r => r.remaining > 0).length;

  const statuses = [...new Set(pos.map(p => p.status))];
  const vRows = pos.filter(po => (!vF || po.vendor_id === vF) && (!sF || po.status === sF));

  const poStatusBadge = (st) => st === 'Material Received' ? <span className="badge success dot">Received</span>
    : st === 'Pending MD Approval' ? <span className="badge warning dot">MD approval</span>
    : st === 'Rejected' ? <span className="badge danger dot">Rejected</span>
    : st === 'On Hold' ? <span className="badge warning dot">On hold</span>
    : <span className="badge dot">{st}</span>;

  // Before POs exist: show the PLANNED vendors from the inquiry allocation so the
  // SO already lists its vendors, with one-click generate.
  if (pos.length === 0) {
    const planned = sourcing && window.vendorPOGroups ? window.vendorPOGroups(state, so, sourcing, getProduct) : [];
    if (planned.length === 0) return (
      <div className="card"><div className="empty">
        <div className="empty-title">No vendors yet</div>
        Vendors come from the inquiry. Add them in Sourcing (Add vendor &amp; quote → Allocate across vendors); they'll appear here and POs auto-generate when you click Start Procurement.
      </div></div>
    );
    return (
      <div className="stack">
        <div className="card"><div className="card-body" style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
          <div className="grow"><strong className="small">Planned vendors from inquiry {sourcing.src_no}</strong><div className="tiny muted">{planned.length} vendor(s) · POs auto-generate on “Start Procurement”, or generate now.</div></div>
          {canProcure && <button className="btn btn-primary" onClick={() => window.generateVendorPOsFromSourcing(so, sourcing, { state, mutate, toast, navigate, getProduct })}><Icon name="cart" size={13}/>Generate {planned.length} Vendor PO(s)</button>}
        </div></div>
        <div className="card"><div className="card-body flush"><table className="t">
          <thead><tr><th>Vendor (planned)</th><th className="num">Items</th><th className="num">Planned cost</th></tr></thead>
          <tbody>{planned.map(g => { const v = getVendor(g.vendor_id); return <tr key={g.vendor_id}><td>{v ? v.name : g.vendor_id}</td><td className="num">{g.items.length}</td><td className="num mono">{inr(g.amount)}</td></tr>; })}</tbody>
          <tfoot><tr><td className="right small">Planned total</td><td></td><td className="num mono"><strong>{inr(planned.reduce((s, g) => s + g.amount, 0))}</strong></td></tr></tfoot>
        </table></div></div>
      </div>
    );
  }

  return (
    <div className="stack">
      {/* Anything Stores has confirmed as arrived, waiting on Purchase. Same
          records and actions as the GRN screen and the Virtual Godown — this is
          simply the third place the person who must act on it might be. */}
      <PendingReceiptsPanel soId={so.id}/>

      {/* Grand totals */}
      <div className="mb-1" style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 8 }}>
        <div className="card"><div className="card-body" style={{ textAlign: 'center' }}><div className="tiny muted">Vendors</div><div style={{ fontSize: 18, fontWeight: 700 }}>{pos.length}</div></div></div>
        <div className="card"><div className="card-body" style={{ textAlign: 'center' }}><div className="tiny muted">Our value</div><div style={{ fontSize: 16, fontWeight: 700 }} className="mono">{inrK(projectSell)}</div></div></div>
        <div className="card"><div className="card-body" style={{ textAlign: 'center' }}><div className="tiny muted">Vendor cost</div><div style={{ fontSize: 16, fontWeight: 700 }} className="mono">{inrK(projectCost)}</div>{projectEstimated > 0 && <div className="tiny muted">{inrK(vendorSpend)} ordered + {inrK(projectEstimated)} still to buy</div>}</div></div>
        <div className="card" style={{ borderColor: projectMargin >= 0 ? 'oklch(0.85 0.06 155)' : 'oklch(0.86 0.08 25)' }}><div className="card-body" style={{ textAlign: 'center', background: projectMargin >= 0 ? 'var(--success-bg)' : 'var(--danger-bg)' }}><div className="tiny muted">Profit margin</div><div style={{ fontSize: 16, fontWeight: 700, color: projectMargin >= 0 ? 'var(--success)' : 'var(--danger)' }} className="mono">{inrK(projectMargin)}</div><div className="tiny" style={{ fontWeight: 600, color: projectMargin >= 0 ? 'var(--success)' : 'var(--danger)' }}>{marginPct >= 0 ? '+' : ''}{marginPct.toFixed(1)}%</div></div></div>
        <div className="card"><div className="card-body" style={{ textAlign: 'center' }}><div className="tiny muted">Payable booked</div><div style={{ fontSize: 16, fontWeight: 700 }} className="mono">{inrK(payableBooked)}</div></div></div>
      </div>

      {/* Engine summary */}
      <div className="card"><div className="card-body small" style={{ display: 'flex', gap: 14, flexWrap: 'wrap', alignItems: 'center' }}>
        <strong className="small">Tracking engine:</strong>
        <span>{shortfalls === 0 ? <span style={{ color: 'var(--success)' }}>✓ all required items are on a PO</span> : <span style={{ color: 'var(--warning)' }}>⚠ {shortfalls} item(s) still need a vendor PO</span>}</span>
        <span>·</span>
        <span>{awaiting === 0 ? <span style={{ color: 'var(--success)' }}>✓ nothing awaiting receipt</span> : <span style={{ color: 'var(--accent)' }}>{awaiting} item(s) awaiting receipt (GRN)</span>}</span>
      </div></div>

      {/* Smart pool suggestions — add stock from the Master Pool & cut vendor need */}
      {PoolPanel && canProcure && <PoolPanel so={so}/>}

      {/* Vendor mapping */}
      <div className="card">
        <div className="card-header">
          <h3 className="card-title">Vendors on this order ({vRows.length})</h3>
          <div style={{ display: 'flex', gap: 6 }}>
            <select className="select" style={{ height: 26, fontSize: 12 }} value={vF} onChange={e => setVF(e.target.value)}>
              <option value="">All vendors</option>
              {[...new Set(pos.map(p => p.vendor_id))].map(vid => { const v = getVendor(vid); return <option key={vid} value={vid}>{v ? v.name : vid}</option>; })}
            </select>
            <select className="select" style={{ height: 26, fontSize: 12 }} value={sF} onChange={e => setSF(e.target.value)}>
              <option value="">All statuses</option>
              {statuses.map(s => <option key={s} value={s}>{s}</option>)}
            </select>
          </div>
        </div>
        <div className="card-body flush">
          <table className="t">
            <thead><tr><th>Vendor · PO</th><th className="num">Cost</th><th className="num">Our value</th><th className="num">Margin</th><th className="num">Received</th><th>e-Bill</th><th>Payable</th><th>Status</th><th></th></tr></thead>
            <tbody>
              {vRows.map(po => {
                const v = getVendor(po.vendor_id);
                const shared = poLinkedSoIds(po).length > 1;
                const cost = poCostForSO(po);
                const our = poOurForSO(po);
                const m = our - cost; const mp = our > 0 ? (m / our) * 100 : 0;
                const ordered = shared ? (po.items || []).reduce((a, it) => a + poLineSoQty(po, it.product_id, so.id), 0) : (po.items || []).reduce((a, it) => a + (it.qty || 0), 0);
                const rec = shared ? grnsFor(po.id).reduce((a, g) => a + (g.items || []).reduce((b, it) => b + grnLineSoQty(it, po, so.id), 0), 0) : grnsFor(po.id).reduce((a, g) => a + (g.items || []).reduce((b, it) => b + (it.accepted || 0), 0), 0);
                const recPct = ordered > 0 ? Math.round(rec / ordered * 100) : 0;
                const vis = viFor(po.id); const booked = vis.filter(x => x.status === 'Booked').length;
                const open = !!expanded[po.id]; const canEdit = editablePO(po); const editOpen = poOpenForEdit(po);
                return (
                  <Fragment key={po.id}>
                  <tr onClick={() => setExpanded(e => ({ ...e, [po.id]: !open }))} style={{ cursor: 'pointer' }}>
                    <td><div style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Icon name={open ? 'chevronDown' : 'chevronRight'} size={12}/><div><strong>{v ? v.name : po.vendor_id}</strong><div className="tiny muted mono">{po.po_no} · {(po.items || []).length} item(s)</div></div></div></td>
                    <td className="num mono">{inr(cost)}{shared && <div className="tiny muted">of {inr(po.amount)} total</div>}</td>
                    <td className="num mono">{inr(our)}</td>
                    <td className="num mono" style={{ color: m >= 0 ? 'var(--success)' : 'var(--danger)' }}>{inr(m)}<div className="tiny">{mp >= 0 ? '+' : ''}{mp.toFixed(1)}%</div></td>
                    <td className="num">{recPct}%</td>
                    <td>{po.ebill && po.ebill.generated ? <span className="badge success dot">e-Bill</span> : <span className="badge dot">—</span>}</td>
                    <td>{vis.length === 0 ? <span className="muted tiny">awaiting</span> : booked ? <span className="badge success dot">{booked} booked</span> : <span className="badge warning dot">{vis.length} pending</span>}</td>
                    <td>{poStatusBadge(po.status)}</td>
                    <td onClick={e => e.stopPropagation()}>
                      {po.status === 'Pending MD Approval'
                        ? (canApprove
                            ? <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
                                {po.pending_change && <div className="tiny" style={{ width: '100%', textAlign: 'right', color: 'var(--accent)' }}>→ {getVendor(po.pending_change.vendor_id)?.name} ({inrK(po.pending_change.amount)})</div>}
                                <button className="btn btn-sm btn-primary" onClick={() => reviewPo(po, true)}>Approve</button>
                                {!po.pending_change && <button className="btn btn-sm" onClick={() => setPoStatus(po, 'On Hold', `${po.po_no} on hold`)}>Hold</button>}
                                <button className="btn btn-sm btn-danger" onClick={() => reviewPo(po, false)}>Reject</button>
                              </div>
                            : <span className="badge warning dot">awaiting MD{po.pending_change ? ' · vendor change' : ''}</span>)
                        : po.status === 'On Hold'
                          ? (canApprove ? <button className="btn btn-sm btn-primary" onClick={() => setPoStatus(po, 'Issued', `${po.po_no} resumed`)}>Resume</button> : <span className="muted tiny">on hold</span>)
                          : po.status === 'Rejected'
                            ? <span className="muted tiny">rejected</span>
                            : canProcure && rec < ordered
                              ? <button className="btn btn-sm btn-primary" onClick={() => setReceivePo(po)}><Icon name="package" size={11}/>Receive</button>
                              : rec >= ordered && ordered > 0 ? <span className="badge success dot">Received</span> : null}
                    </td>
                  </tr>
                  {open && (
                    <tr className="subrow"><td colSpan="9" style={{ background: 'var(--surface-2)', padding: 0 }}>
                      <div style={{ padding: '8px 12px' }}>
                        <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
                          <strong className="small">Line items on {po.po_no} → {v ? v.name : po.vendor_id}</strong>
                          {editOpen ? <span className="badge accent tiny">editable</span> : <span className="badge tiny" title="Locked once received / awaiting MD">read-only</span>}
                          <div className="grow"/>
                          {canEdit && (
                            <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                              <span className="tiny muted">Replace vendor:</span>
                              <select className="select" style={{ height: 24, fontSize: 12 }} value={po.vendor_id} onChange={e => changePOVendor(po, e.target.value)}>
                                {state.vendors.map(vd => <option key={vd.id} value={vd.id}>{vd.name}</option>)}
                              </select>
                            </span>
                          )}
                          <button className="btn btn-sm btn-ghost" onClick={() => navigate(`vendor-pos/${po.id}`)}><Icon name="arrowRight" size={11}/>Open PO</button>
                        </div>
                        <table className="t" style={{ background: 'var(--surface)' }}>
                          <thead><tr><th>Line item</th><th>Code</th><th className="num">Qty</th><th className="num">Rate ₹</th><th className="num">Line ₹</th><th className="num">Received</th>{editOpen && <th style={{ width: 28 }}></th>}</tr></thead>
                          <tbody>
                            {(po.items || []).map(it => {
                              const p = getProduct(it.product_id) || { name: it.product_id, code: it.product_id };
                              const itRec = grnsFor(po.id).reduce((a, g) => a + (g.items || []).filter(y => y.product_id === it.product_id).reduce((b, y) => b + (y.accepted || 0), 0), 0);
                              const locked = !lineEditable(po, it.product_id);
                              const shared = Array.isArray(it.so_alloc) && it.so_alloc.length > 1;
                              const splitKey = po.id + '|' + it.product_id;
                              return (
                                <Fragment key={it.product_id}>
                                <tr>
                                  <td><strong>{p.name}</strong>{locked && editOpen && <span className="badge tiny" style={{ marginLeft: 6 }} title="Locked once this item is received">locked</span>}</td>
                                  <td className="mono small muted">{p.code}</td>
                                  <td className="num">{editOpen && !locked ? <input type="number" min="0" className="input mono" value={it.qty} onChange={e => setItemQty(po, it.product_id, e.target.value)} style={{ width: 64, height: 24, textAlign: 'right' }}/> : it.qty}</td>
                                  <td className="num">{editOpen && !locked ? <input type="number" min="0" className="input mono" value={it.rate} onChange={e => setItemRate(po, it.product_id, e.target.value)} style={{ width: 84, height: 24, textAlign: 'right' }}/> : inr(it.rate)}</td>
                                  <td className="num mono">{inr((Number(it.qty) || 0) * (Number(it.rate) || 0))}</td>
                                  <td className="num">{itRec || <span className="muted">0</span>}</td>
                                  {editOpen && <td>{!locked && <button className="btn btn-ghost btn-sm" title="Remove item from this PO" onClick={() => removeItem(po, it.product_id)}><Icon name="x" size={11} color="var(--danger)"/></button>}</td>}
                                </tr>
                                {editOpen && !locked && (
                                  <tr><td colSpan={7} style={{ padding: shared || splitOpen[splitKey] ? undefined : '0 8px 6px' }}>
                                    {!splitOpen[splitKey] && (
                                      <button type="button" className="btn btn-ghost btn-sm" onClick={() => setSplitOpen(m => ({ ...m, [splitKey]: true }))}>
                                        <Icon name="arrowLeftRight" size={11}/>{shared ? `Split across ${it.so_alloc.length} SOs — edit` : 'Split this line across SOs'}
                                      </button>
                                    )}
                                    {splitOpen[splitKey] && (
                                      <LinePoSplitEditor state={state} po={po} it={it}
                                        onSave={(so_alloc) => { setItemSoAlloc(po, it.product_id, so_alloc); setSplitOpen(m => ({ ...m, [splitKey]: false })); }}
                                        onCancel={() => setSplitOpen(m => ({ ...m, [splitKey]: false }))}/>
                                    )}
                                  </td></tr>
                                )}
                                </Fragment>
                              );
                            })}
                            {(po.items || []).length === 0 && <tr><td colSpan={editOpen ? 7 : 6}><div className="empty">No items.</div></td></tr>}
                          </tbody>
                        </table>
                        {editOpen && (
                          <div style={{ display: 'flex', gap: 6, alignItems: 'center', marginTop: 6 }}>
                            <select className="select" style={{ height: 26, fontSize: 12, width: 240 }} value={addSel[po.id] || ''} onChange={e => setAddSel(m => ({ ...m, [po.id]: e.target.value }))}>
                              <option value="">+ Add line item to this vendor…</option>
                              {state.products.filter(p => !(po.items || []).some(it => it.product_id === p.id)).map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
                            </select>
                            <button className="btn btn-sm" disabled={!addSel[po.id]} onClick={() => addItemToPO(po)}><Icon name="plus" size={11}/>Add</button>
                            <span className="tiny muted">Edits recompute the PO amount & re-check the MD approval threshold. A received line is locked; others on the same PO stay editable.</span>
                          </div>
                        )}
                      </div>
                    </td></tr>
                  )}
                  </Fragment>
                );
              })}
            </tbody>
            <tfoot><tr><td className="right small">Totals</td><td className="num mono"><strong>{inr(vendorSpend)}</strong></td><td className="num mono">{inr(pos.reduce((a, p) => a + poOurForSO(p), 0))}</td><td className="num mono" style={{ color: projectMargin >= 0 ? 'var(--success)' : 'var(--danger)' }}><strong>{inr(pos.reduce((a, p) => a + poOurForSO(p), 0) - vendorSpend)}</strong></td><td colSpan="5"></td></tr></tfoot>
          </table>
        </div>
        {canProcure && <div className="card-body" style={{ borderTop: '1px solid var(--border)', display: 'flex', gap: 8 }}><button className="btn btn-sm btn-primary" onClick={() => setShowSplit(true)}><Icon name="arrowLeftRight" size={12}/>Split across vendors</button><button className="btn btn-sm" onClick={() => navigate('grn/new')}><Icon name="package" size={12}/>Receive material (GRN)</button><button className="btn btn-sm" onClick={() => navigate('three-way')}><Icon name="check" size={12}/>Vendor invoices / 3-way</button></div>}
      </div>

      {/* Item tracking */}
      <div className="card">
        <div className="card-header">
          <h3 className="card-title">Item tracking</h3>
          <input className="input search" placeholder="Search item…" value={q} onChange={e => setQ(e.target.value)} style={{ height: 26, width: 200 }}/>
        </div>
        <div className="card-body flush">
          <table className="t">
            <thead><tr><th>Item</th>{im && <th>For</th>}<th className="num">Required</th><th className="num">On PO</th><th className="num">Received</th><th className="num">Remaining</th><th>Status</th></tr></thead>
            <tbody>
              {track.map(r => (
                <tr key={r.pid}>
                  <td>{r.p ? r.p.name : r.pid}<div className="tiny muted mono">{r.p ? r.p.code : ''}</div></td>
                  {im && <td><span className={`badge tiny ${r.type === 'Supply' ? '' : 'accent'}`}>{r.type}</span></td>}
                  <td className="num">{r.req}</td>
                  <td className="num">{r.onPO}{r.shortfall > 0 && <div className="tiny" style={{ color: 'var(--warning)' }}>short {r.shortfall}</div>}
                    {r.onPOBreakdown.length > 1 && <div className="tiny muted">from {r.onPOBreakdown.map((b, i) => (
                      <Fragment key={b.po_id}>{i > 0 && ', '}<a style={{ cursor: 'pointer', textDecoration: 'underline' }} onClick={() => navigate(`vendor-pos/${b.po_id}`)}>{b.po_no}</a> ({qty(b.qty)})</Fragment>
                    ))}</div>}
                  </td>
                  <td className="num">{r.recv}
                    {r.recvBreakdown.length > 1 && <div className="tiny muted">from {r.recvBreakdown.map((b, i) => (
                      <Fragment key={b.po_id}>{i > 0 && ', '}<a style={{ cursor: 'pointer', textDecoration: 'underline' }} onClick={() => navigate(`vendor-pos/${b.po_id}`)}>{b.po_no}</a> ({qty(b.qty)})</Fragment>
                    ))}</div>}
                  </td>
                  <td className="num">{r.remaining > 0 ? <strong style={{ color: 'var(--accent)' }}>{r.remaining}</strong> : <span className="muted">0</span>}</td>
                  <td>{r.shortfall > 0 ? <span className="badge warning dot">Add PO</span> : r.remaining > 0 ? <span className="badge accent dot">Awaiting</span> : <span className="badge success dot">Complete</span>}</td>
                </tr>
              ))}
              {track.length === 0 && <tr><td colSpan={im ? 7 : 6}><div className="empty">No items match.</div></td></tr>}
            </tbody>
          </table>
        </div>
      </div>

      {/* Implementation BOQ — the site items the supervisor listed, tracked have vs remaining */}
      {im && (() => {
        const boq = im.boq || [];
        const rows = boq.map(b => {
          const req = Number(b.qty) || 0;
          const recv = b.product_id ? (receivedByProd[b.product_id] || 0) : 0;
          const onPO = b.product_id ? (onPOByProd[b.product_id] || 0) : 0;
          return { ...b, req, recv, onPO, remaining: Math.max(0, req - recv), procurable: !!b.product_id };
        });
        const done = rows.filter(r => !r.procurable || r.remaining <= 0).length;
        return (
          <div className="card" style={{ borderLeft: '3px solid var(--accent)' }}>
            <div className="card-header">
              <div><h3 className="card-title">Implementation BOQ</h3><span className="card-sub">{im.status || 'BOQ Pending'} · {done}/{rows.length} handled · billed {inr(im.hourly_rate || 0)}/hr (hours from daily logs)</span></div>
            </div>
            <div className="card-body flush">
              {rows.length === 0 ? <div className="empty">No BOQ yet — the supervisor prepares it on the inquiry.</div> : (
                <table className="t">
                  <thead><tr><th>Site item</th><th className="num">Required</th><th className="num">On PO</th><th className="num">Received</th><th className="num">Remaining</th><th>Status</th></tr></thead>
                  <tbody>{rows.map(r => (
                    <tr key={r.id}>
                      <td>{r.name}{r.procurable ? '' : <span className="badge tiny" style={{ marginLeft: 4 }}>service / manual</span>}{r.uom ? <span className="tiny muted"> · {r.uom}</span> : ''}</td>
                      <td className="num">{r.req}</td>
                      <td className="num">{r.procurable ? r.onPO : <span className="muted">—</span>}</td>
                      <td className="num">{r.procurable ? r.recv : <span className="muted">—</span>}</td>
                      <td className="num">{r.procurable ? (r.remaining > 0 ? <strong style={{ color: 'var(--accent)' }}>{r.remaining}</strong> : <span className="muted">0</span>) : <span className="muted">n/a</span>}</td>
                      <td>{!r.procurable ? <span className="badge dot">Tracked on site</span> : r.remaining <= 0 ? <span className="badge success dot">Received</span> : r.onPO >= r.req ? <span className="badge accent dot">Awaiting</span> : <span className="badge warning dot">Procure</span>}</td>
                    </tr>
                  ))}</tbody>
                </table>
              )}
            </div>
            <div className="card-body" style={{ borderTop: '1px solid var(--border)' }}><div className="tiny muted">Catalogue BOQ items are procured with the supply items above (pool-first, then vendor PO). Service/manual items (labour, etc.) are tracked via the supervisor's daily usage. Everything lands in the same Virtual Godown.</div></div>
          </div>
        );
      })()}

      {showSplit && <SplitAllocatorModal so={so} onClose={() => setShowSplit(false)}/>}
      {receivePo && <ReceiveModal po={receivePo} onClose={() => setReceivePo(null)}/>}
    </div>
  );
}

window.SOVendorPOsTab = SOVendorPOsTab;

// Create one Vendor PO per vendor from a split allocation { product_id: [{vendor_id, qty, rate}] }.
function generateSplitVendorPOs(so, alloc, ctx) {
  const { state, mutate, toast, navigate } = ctx;
  const groups = {};
  Object.entries(alloc || {}).forEach(([pid, rows]) => (rows || []).forEach(r => {
    const qty = Number(r.qty) || 0; if (qty <= 0 || !r.vendor_id) return;
    (groups[r.vendor_id] = groups[r.vendor_id] || []).push({ product_id: pid, qty, rate: Number(r.rate) || 0 });
  }));
  const entries = Object.entries(groups);
  if (!entries.length) { toast('Allocate at least one item to a vendor'); return; }
  // Issued one at a time, each aware of the ones already handed out in this
  // same action, so a batch of POs cannot collide.
  const __issued = state.vendor_pos.map(p => p.po_no);
  const nextPo = () => { const n = vendorPoNo({ vendor_pos: __issued.map(x => ({ po_no: x })) }, TODAY); __issued.push(n); return n; };
  const expected = (() => { const d = new Date(TODAY); d.setDate(d.getDate() + 7); return d.toISOString().slice(0, 10); })();
  const pos = entries.map(([vid, items], i) => {
    const amount = items.reduce((s, it) => s + it.qty * it.rate, 0);
    return { id: 'po-' + Date.now() + '-' + i, po_no: nextPo(), so_id: so.id, vendor_id: vid, date: TODAY, expected, status: amount > (state.config.vendor_po_md_threshold ?? 500000) ? 'Pending MD Approval' : 'Issued', amount, items, ebill: {}, source: 'sourcing-split' };
  });
  const anyMD = pos.some(p => p.status === 'Pending MD Approval');
  mutate(s => ({
    ...s,
    vendor_pos: [...pos, ...s.vendor_pos],
    sales_orders: s.sales_orders.map(x => x.id === so.id ? { ...x, status: soAdvanceStatus(x.status, 'Procurement Started') } : x),
    notifications: [{ id: 'n-split-' + Date.now(), kind: 'po', text: `${pos.length} split Vendor PO(s) raised for ${so.so_no}${anyMD ? ' · some need MD approval' : ''}`, date: TODAY, read: false, role: anyMD ? 'Managing Director' : 'Stores' }, ...s.notifications],
  }), { action: 'split-po', entity: 'SalesOrder', entity_id: so.id });
  toast(`${pos.length} Vendor PO(s) created — quantity split across vendors`, 'success');
  navigate(`sales-orders/${so.id}`);
}

// Split each still-to-order item's quantity across multiple vendors → split POs.
function SplitAllocatorModal({ so, onClose }) {
  const { state, mutate, navigate, getVendor, getProduct } = useStore();
  const toast = useToast();
  const sourcing = window.soSourcing ? window.soSourcing(state, so.id) : null;
  const candIds = (sourcing && (sourcing.quote_vendors || []).length) ? sourcing.quote_vendors : state.vendors.map(v => v.id);
  const required = soReqComponents(so);
  const onPO = {};
  state.vendor_pos.filter(p => poServesSO(p, so.id)).forEach(po => (po.items || []).forEach(it => { onPO[it.product_id] = (onPO[it.product_id] || 0) + poLineSoQty(po, it.product_id, so.id); }));
  const items = Object.keys(required).map(pid => ({ pid, p: getProduct(pid), required: required[pid], onPO: onPO[pid] || 0, toOrder: Math.max(0, required[pid] - (onPO[pid] || 0)) })).filter(x => x.toOrder > 0);
  const rateOf = (vid, p) => (sourcing && sourcing.prices && sourcing.prices[p.id] && sourcing.prices[p.id][vid] != null) ? sourcing.prices[p.id][vid] : (window.vendorUnitPrice ? window.vendorUnitPrice(vid, p) : (p ? (p.buy || 0) : 0));
  const [alloc, setAlloc] = React.useState(() => {
    const a = {};
    items.forEach(x => { const def = (sourcing && sourcing.picks && sourcing.picks[x.pid]) || candIds[0]; a[x.pid] = [{ vendor_id: def, qty: x.toOrder, rate: rateOf(def, x.p) }]; });
    return a;
  });
  const setRow = (pid, ri, patch) => setAlloc(a => ({ ...a, [pid]: a[pid].map((r, i) => i === ri ? { ...r, ...patch } : r) }));
  const addRow = (pid) => setAlloc(a => ({ ...a, [pid]: [...(a[pid] || []), { vendor_id: candIds[0], qty: 0, rate: rateOf(candIds[0], items.find(x => x.pid === pid).p) }] }));
  const removeRow = (pid, ri) => setAlloc(a => ({ ...a, [pid]: a[pid].filter((_, i) => i !== ri) }));

  const vendorSet = new Set();
  let totalAmt = 0;
  Object.values(alloc).forEach(rows => rows.forEach(r => { const q = Number(r.qty) || 0; if (q > 0 && r.vendor_id) { vendorSet.add(r.vendor_id); totalAmt += q * (Number(r.rate) || 0); } }));

  return (
    <Modal title={`Split across vendors — ${so.so_no}`} onClose={onClose} size="lg" footer={
      <>
        <button className="btn" onClick={onClose}>Cancel</button>
        <button className="btn btn-primary" disabled={vendorSet.size === 0} onClick={() => { generateSplitVendorPOs(so, alloc, { state, mutate, toast, navigate, getVendor }); onClose(); }}>Generate {vendorSet.size} PO(s) · {inr(totalAmt)}</button>
      </>
    }>
      {items.length === 0 ? (
        <div className="empty"><div className="empty-title">Nothing left to order</div>Every required item is already on a Vendor PO.</div>
      ) : (
        <>
          <div className="tiny muted mb-2" style={{ padding: 10, background: 'var(--accent-bg)', borderRadius: 4 }}>
            For each item, give each vendor a quantity (and rate). The split is auto-grouped into one Vendor PO per vendor. Rate prefills from the inquiry quote / estimate.
          </div>
          {items.map(x => {
            const rows = alloc[x.pid] || [];
            const allocated = rows.reduce((s, r) => s + (Number(r.qty) || 0), 0);
            const remain = x.toOrder - allocated;
            return (
              <div className="card mb-2" key={x.pid}>
                <div className="card-header">
                  <div><strong>{x.p ? x.p.name : x.pid}</strong><div className="tiny muted mono">{x.p ? x.p.code : ''} · required {x.required} · on PO {x.onPO} · to allocate {x.toOrder}</div></div>
                  <span className={`badge ${remain === 0 ? 'success' : remain < 0 ? 'danger' : 'warning'} dot`}>{remain === 0 ? 'balanced' : remain < 0 ? `over ${-remain}` : `${remain} left`}</span>
                </div>
                <div className="card-body flush">
                  <table className="t">
                    <thead><tr><th>Vendor</th><th className="num">Qty</th><th className="num">Rate ₹</th><th className="num">Line</th><th style={{ width: 28 }}></th></tr></thead>
                    <tbody>
                      {rows.map((r, ri) => (
                        <tr key={ri}>
                          <td><select className="select" value={r.vendor_id} onChange={e => setRow(x.pid, ri, { vendor_id: e.target.value, rate: rateOf(e.target.value, x.p) })} style={{ height: 26, fontSize: 12 }}>{candIds.map(vid => { const v = getVendor(vid); return <option key={vid} value={vid}>{v ? v.name : vid}</option>; })}</select></td>
                          <td className="num"><input type="number" min="0" className="input mono" value={r.qty} onChange={e => setRow(x.pid, ri, { qty: e.target.value })} style={{ width: 80, textAlign: 'right', height: 26 }}/></td>
                          <td className="num"><input type="number" min="0" className="input mono" value={r.rate} onChange={e => setRow(x.pid, ri, { rate: e.target.value })} style={{ width: 90, textAlign: 'right', height: 26 }}/></td>
                          <td className="num mono">{inr((Number(r.qty) || 0) * (Number(r.rate) || 0))}</td>
                          <td>{rows.length > 1 && <button className="btn btn-ghost btn-sm" onClick={() => removeRow(x.pid, ri)}><Icon name="x" size={11}/></button>}</td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                  <div style={{ padding: '6px 14px' }}><button className="btn btn-sm" onClick={() => addRow(x.pid)}><Icon name="plus" size={11}/>Add vendor</button></div>
                </div>
              </div>
            );
          })}
        </>
      )}
    </Modal>
  );
}

window.SplitAllocatorModal = SplitAllocatorModal;

// ===== Vendor PO List ===== (payables side — kept separate from client billing)
function VendorPOList() {
  const { state, navigate, getVendor, getSO, getCustomer, currentUser, getUser } = useStore();
  const [showPO, setShowPO] = React.useState(false);
  const [showBulkPO, setShowBulkPO] = React.useState(false);   // one PO combined across several SOs
  const [allocSO, setAllocSO] = React.useState('');     // bulk assign for one SO
  const [pickAlloc, setPickAlloc] = React.useState(false);
  const [search, setSearch] = React.useState('');
  const [vendorF, setVendorF] = React.useState('');
  const [statusF, setStatusF] = React.useState('');
  const [groupBySO, setGroupBySO] = React.useState(true);
  const role = getUser(currentUser)?.role;
  const canCreate = ['Purchase', 'Project Manager', 'Stores', 'Org Admin'].includes(role);

  const poStatusBadge = (st) => st === 'Material Received' ? <span className="badge success dot">Received</span>
    : st === 'In Transit' ? <span className="badge accent dot">In transit</span>
    : st === 'Pending MD Approval' ? <span className="badge warning dot">MD approval</span>
    : <span className="badge dot">{st}</span>;

  const rows = state.vendor_pos.filter(po => {
    if (vendorF && po.vendor_id !== vendorF) return false;
    if (statusF && po.status !== statusF) return false;
    if (search) {
      const v = getVendor(po.vendor_id); const so = getSO(po.so_id); const cust = so && getCustomer(so.customer_id);
      const blob = `${po.po_no} ${v?.name || ''} ${so?.so_no || ''} ${cust?.name || ''}`.toLowerCase();
      if (!blob.includes(search.toLowerCase())) return false;
    }
    return true;
  });
  const statuses = [...new Set(state.vendor_pos.map(p => p.status))];

  const Row = (po) => {
    const v = getVendor(po.vendor_id);
    const so = getSO(po.so_id);
    const cust = so ? getCustomer(so.customer_id) : null;
    const ebilled = po.ebill && po.ebill.generated;
    const linkedCount = window.poLinkedSoIds ? window.poLinkedSoIds(po).length : 1;
    return (
      <tr key={po.id} onClick={() => navigate(`vendor-pos/${po.id}`)} style={{ cursor: 'pointer' }}>
        <td><a className="mono">{po.po_no}</a>{po.source === 'sourcing' && <div className="tiny muted">from inquiry</div>}{linkedCount > 1 && <div className="tiny" style={{ color: 'var(--accent)' }}>combined · {linkedCount} SOs</div>}</td>
        <td>{v ? v.name : '—'}<div className="tiny muted mono">{v?.gstin}</div></td>
        {!groupBySO && <td className="mono small">{so?.so_no}<div className="tiny muted">{cust?.name}</div></td>}
        <td className="mono small">{fmtDate(po.date)}</td>
        <td className="num">{inr(po.amount)}</td>
        <td>{poStatusBadge(po.status)}</td>
        <td>{ebilled ? <span className="badge success dot" title={po.ebill.no}>e-Bill</span> : <span className="badge dot">—</span>}</td>
        <td><Icon name="chevronRight" size={12}/></td>
      </tr>
    );
  };

  // Group by SO (project) so each project's selected vendors read clearly.
  const groups = {};
  rows.forEach(po => { (groups[po.so_id || '—'] = groups[po.so_id || '—'] || []).push(po); });

  return (
    <div className="page">
      <div className="page-header">
        <div>
          <h1 className="page-title">Vendor Purchase Orders</h1>
          <div className="page-sub">Money we pay vendors (payables) · {state.vendor_pos.length} POs · FY {state.org.fiscal_year} · client invoices live separately under Invoices</div>
        </div>
        <div className="page-actions">
          <button className={`btn ${groupBySO ? 'btn-primary' : ''}`} onClick={() => setGroupBySO(g => !g)}><Icon name="layers" size={13}/>{groupBySO ? 'Grouped by project' : 'Flat list'}</button>
          {canCreate && <button className="btn btn-primary" onClick={() => setPickAlloc(true)}
            title="Pick vendors and prices for a whole order at once, then raise every PO together">
            <Icon name="layers" size={13}/>Assign vendors &amp; prices</button>}
          {canCreate && <button className="btn" onClick={() => setShowBulkPO(true)}
            title="One PO covering several Sales Orders that independently need the same item">
            <Icon name="arrowLeftRight" size={13}/>Bulk PO</button>}
          {canCreate && <button className="btn" onClick={() => setShowPO(true)}><Icon name="plus" size={13}/>Single PO</button>}
        </div>
      </div>
      {showPO && <CreateVendorPOModal onClose={() => setShowPO(false)}/>}
      {showBulkPO && <BulkVendorPOModal onClose={() => setShowBulkPO(false)}/>}
      {pickAlloc && <AllocSOPicker onClose={() => setPickAlloc(false)}
        onPick={id => { setPickAlloc(false); setAllocSO(id); }}/>}
      {allocSO && <VendorAllocator soId={allocSO} onClose={() => setAllocSO('')}/>}

      <div className="card">
        <div className="filter-bar">
          <input className="input search" placeholder="Search PO no, vendor, SO, customer…" value={search} onChange={e => setSearch(e.target.value)} style={{ flex: '0 0 240px' }}/>
          <select className="select" style={{ width: 150 }} value={vendorF} onChange={e => setVendorF(e.target.value)}>
            <option value="">All vendors</option>
            {state.vendors.map(v => <option key={v.id} value={v.id}>{v.name}</option>)}
          </select>
          <select className="select" style={{ width: 150 }} value={statusF} onChange={e => setStatusF(e.target.value)}>
            <option value="">All statuses</option>
            {statuses.map(s => <option key={s} value={s}>{s}</option>)}
          </select>
          <div className="grow"/>
          <span className="muted small">{rows.length} shown</span>
        </div>

        {state.vendor_pos.length === 0 ? (
          <div className="card-body"><div className="empty"><div className="empty-title">No Vendor POs yet</div>Approve a sourced Sales Order, then generate POs for the selected vendors — they appear here, grouped by project.</div></div>
        ) : groupBySO ? (
          Object.entries(groups).map(([soId, pos]) => {
            const so = getSO(soId); const cust = so ? getCustomer(so.customer_id) : null;
            const total = pos.reduce((s, p) => s + (p.amount || 0), 0);
            return (
              <div key={soId} style={{ borderTop: '1px solid var(--border)' }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 14px', background: 'var(--bg-subtle, var(--surface))' }}>
                  <div className="small"><strong className="mono" onClick={() => so && navigate(`sales-orders/${soId}`)} style={{ cursor: so ? 'pointer' : 'default' }}>{so ? so.so_no : 'Unlinked'}</strong>{cust && <span className="muted"> · {cust.name}</span>} <span className="muted">· {pos.length} vendor PO(s)</span></div>
                  <div className="small mono">{inr(total)}</div>
                </div>
                <div className="table-wrap"><table className="t"><thead><tr>
                  <th>PO No</th><th>Vendor</th><th>Date</th><th className="num">Amount</th><th>Status</th><th>e-Bill</th><th></th>
                </tr></thead><tbody>{pos.map(Row)}</tbody></table></div>
              </div>
            );
          })
        ) : (
          <div className="table-wrap"><table className="t"><thead><tr>
            <th>PO No</th><th>Vendor</th><th>For SO · Customer</th><th>Date</th><th className="num">Amount</th><th>Status</th><th>e-Bill</th><th></th>
          </tr></thead><tbody>{rows.map(Row)}</tbody></table></div>
        )}
      </div>
    </div>
  );
}

function VendorPODetail({ poId }) {
  const { state, navigate, mutate, getVendor, getSO, getCustomer, getProduct, currentUser, getUser } = useStore();
  const toast = useToast();
  const po = state.vendor_pos.find(p => p.id === poId);
  if (!po) return <div className="page"><div className="empty">{state.loaded ? 'PO not found' : 'Loading…'}</div></div>;
  const role = currentUser ? getUser(currentUser)?.role : '';
  const canApprovePO = ['Managing Director', 'Org Admin'].includes(role);
  const canProcurePO = ['Purchase', 'Project Manager', 'Stores', 'Org Admin'].includes(role);
  const blocked = ['Pending MD Approval', 'Rejected', 'On Hold'].includes(po.status);
  const setPoStatus = (status, msg) => {
    mutate(s => ({ ...s, vendor_pos: s.vendor_pos.map(p => p.id === po.id ? { ...p, status } : p), notifications: [{ id: 'n-poapp-' + Date.now(), kind: 'po', text: `${po.po_no} ${status === 'Issued' ? 'resumed' : status === 'Rejected' ? 'rejected' : 'on hold'} by MD`, date: TODAY, read: false, role: 'Purchase' }, ...s.notifications] }), { action: 'po-status', entity: 'VendorPO', entity_id: po.id });
    toast(msg, status === 'Rejected' ? '' : 'success');
  };
  const reviewPo = (approve) => {
    const changed = !!po.pending_change;
    mutate(s => ({ ...s, vendor_pos: s.vendor_pos.map(p => p.id === po.id ? applyPOReview(p, approve) : p), notifications: [{ id: 'n-porev-' + Date.now(), kind: 'po', text: `${po.po_no} ${approve ? (changed ? 'vendor change approved' : 'approved · ready to receive') : (changed ? 'vendor change declined' : 'rejected')} by MD`, date: TODAY, read: false, role: 'Purchase' }, ...s.notifications] }), { action: 'po-review', entity: 'VendorPO', entity_id: po.id });
    toast(approve ? `${po.po_no} approved` : `${po.po_no} ${changed ? 'change declined' : 'rejected'}`, approve ? 'success' : '');
  };
  const v = getVendor(po.vendor_id);
  const so = getSO(po.so_id);
  const cust = so ? getCustomer(so.customer_id) : null;

  // Real tax, from each line's own config — never a flat 18%. The same
  // per-line engine the e-Bill's own Document tab already uses
  // (poEbillHtml), so this card's header/footer total can never drift from
  // what the printable document actually shows. See docs/po-tax.md.
  const subtotal = po.items.reduce((s,i) => s + i.qty * i.rate, 0);
  const taxRows = po.items.map(it => ({ amount: (Number(it.qty) || 0) * (Number(it.rate) || 0), taxes: poLineTaxes(po, it.product_id) }));
  const taxBreakdown = taxSummary(taxRows);
  const gst = taxBreakdown.reduce((a, t) => a + t.amount, 0);
  const grand = subtotal + gst;
  const ebilled = po.ebill && po.ebill.generated;
  const [showVI, setShowVI] = React.useState(false);
  const [showChange, setShowChange] = React.useState(false);
  const [showTransit, setShowTransit] = React.useState(false);
  const [showEbill, setShowEbill] = React.useState(false);
  const [showReceive, setShowReceive] = React.useState(false);
  // The vendor's OWN part numbers for the items on this PO. An org running the
  // procurement profile prints the PO in the vendor's catalogue; everyone else
  // keeps printing ours. Unmapped lines always fall back to our names, so the
  // PO is never blank — it is flagged instead.
  const vendorAliases = useAliasMap('vendor', po.vendor_id);
  const poLang = wf('po_item_language');
  const inVendorLang = poLang === 'vendor';
  const unmapped = inVendorLang
    ? po.items.filter(it => { const a = vendorAliases[it.product_id]; return !a || !(a.code || a.name); }).length
    : 0;
  const dispatch = po.dispatch_info || {};
  const inTransit = !!(dispatch.lr_no || dispatch.carrier || dispatch.shipped_on);

  // Sibling POs for the same project (SO) — the vendors selected for this order.
  const siblings = state.vendor_pos.filter(p => p.so_id === po.so_id);
  // Vendor invoice booked against this PO (payables / 3-way side).
  const vInv = (state.vendor_invoices || []).find(x => x.po_id === po.id);
  const grn = state.grns.find(g => g.po_id === po.id);
  // Simple, mostly-automatic lifecycle for non-tech users.
  // Only show the stages this organization actually has. An e-Bill step on a
  // company with e-invoicing switched off is a step that never completes, and a
  // permanently grey stage reads as something being wrong.
  const stages = [
    { label: 'Issued', done: true },
    { label: 'Received', done: po.status === 'Material Received' || !!grn },
    ...(featureOn('e_invoice') ? [{ label: 'e-Bill', done: !!ebilled }] : []),
    { label: 'Vendor Invoice', done: !!vInv },
    { label: 'Booked', done: !!(vInv && vInv.status === 'Booked') },
  ];

  const genEbill = () => {
    const ebill = {
      no: poEbillNo(po),
      irn: (po.id + po.po_no).replace(/[^a-zA-Z0-9]/g, '').slice(0, 16).toUpperCase(),
      date: TODAY, amount: grand, generated: true,
    };
    mutate(s => ({ ...s, vendor_pos: s.vendor_pos.map(p => p.id === po.id ? { ...p, ebill } : p) }),
      { action: 'ebill', entity: 'VendorPO', entity_id: po.id });
    toast('Vendor PO e-Bill generated & stored on this PO', 'success');
  };

  return (
    <div className="page">
      <div className="page-header">
        <div>
          <div className="muted tiny mb-1" style={{ cursor: 'pointer' }} onClick={() => navigate('vendor-pos')}>
            <Icon name="chevronLeft" size={12}/> Vendor POs
          </div>
          <h1 className="page-title">
            <span className="mono">{po.po_no}</span>
            <span className="badge dot ml-2" style={{ marginLeft: 8 }}>{po.status}</span>
            {po.source === 'sourcing' && <span className="badge accent" style={{ marginLeft: 6 }}>from inquiry</span>}
            {window.poLinkedSoIds && window.poLinkedSoIds(po).length > 1 && <span className="badge accent" style={{ marginLeft: 6 }} title={window.poLinkedSoIds(po).map(id => getSO(id)?.so_no).filter(Boolean).join(', ')}><Icon name="arrowLeftRight" size={11}/>combined · {window.poLinkedSoIds(po).length} SOs</span>}
          </h1>
          <div className="page-sub">Payable to <strong>{v.name}</strong> · For project <span className="mono">{so?.so_no}</span>{cust ? ` · ${cust.name}` : ''}</div>
        </div>
        <div className="page-actions">
          {po.status === 'Pending MD Approval' && canApprovePO && <>
            <button className="btn btn-primary" onClick={() => reviewPo(true)}><Icon name="check" size={13}/>{po.pending_change ? 'Approve change' : 'Approve'}</button>
            {!po.pending_change && <button className="btn" onClick={() => setPoStatus('On Hold', `${po.po_no} on hold`)}><Icon name="alert" size={13}/>Hold</button>}
            <button className="btn btn-danger" onClick={() => reviewPo(false)}>Reject</button>
          </>}
          {po.status === 'On Hold' && canApprovePO && <button className="btn btn-primary" onClick={() => setPoStatus('Issued', `${po.po_no} resumed`)}><Icon name="repeat" size={13}/>Resume</button>}
          {canProcurePO && ['Issued', 'In Transit'].includes(po.status) && !po.pending_change && !grn && <button className="btn" onClick={() => setShowChange(true)}><Icon name="arrowLeftRight" size={13}/>Change vendor</button>}
          {featureOn('e_invoice') && (ebilled
            ? <button className="btn" onClick={() => setShowEbill(true)}><Icon name="receipt" size={13}/>View e-Bill</button>
            : !blocked && <button className="btn btn-primary" onClick={genEbill}><Icon name="receipt" size={13}/>Generate PO e-Bill</button>)}
          {!blocked && (po.status !== 'Material Received'
            ? <button className="btn btn-primary" onClick={() => setShowReceive(true)}><Icon name="package" size={13}/>Receive</button>
            : !vInv
              ? <button className="btn btn-primary" onClick={() => setShowVI(true)}><Icon name="receipt" size={13}/>Record vendor invoice</button>
              : <button className="btn" onClick={() => navigate(`three-way/${vInv.id}`)}><Icon name="check" size={13}/>View 3-way match</button>)}
        </div>
      </div>
      {po.status === 'Pending MD Approval' && <div className="mb-2" style={{ padding: '10px 12px', background: 'var(--warning-bg)', border: '1px solid oklch(0.85 0.09 75)', borderRadius: 'var(--radius)', display: 'flex', gap: 8, alignItems: 'center', fontSize: 12.5 }}><Icon name="alert" size={14} color="var(--warning)"/><span>{po.pending_change
        ? <><strong>Vendor change awaiting MD approval</strong> · switch to <strong>{getVendor(po.pending_change.vendor_id)?.name}</strong> at {inr(po.pending_change.amount)} (was {inr(po.pending_change.old_amount)}, saves {inr(po.pending_change.old_amount - po.pending_change.amount)}). On approval the vendor swaps in automatically and the flow continues.</>
        : <><strong>Awaiting MD approval</strong> · this PO is over the approval threshold ({inr(grand)}). Receiving is blocked until approved.</>}{!canApprovePO && ' Only the Managing Director / Org Admin can approve.'}</span></div>}
      {po.status === 'Rejected' && <div className="mb-2" style={{ padding: '10px 12px', background: 'var(--danger-bg)', borderRadius: 'var(--radius)', fontSize: 12.5 }}><Icon name="alert" size={14} color="var(--danger)"/> <strong>Rejected by MD.</strong> Re-create a PO via the Vendor POs tab if needed.</div>}
      {showVI && <RecordVendorInvoiceModal poId={po.id} onClose={() => setShowVI(false)}/>}
      {showChange && <ChangeVendorModal po={po} onClose={() => setShowChange(false)}/>}
      {showTransit && <InTransitModal po={po} onClose={() => setShowTransit(false)}/>}
      {showEbill && <POEbillModal po={po} onClose={() => setShowEbill(false)}/>}
      {showReceive && <ReceiveModal po={po} onClose={() => setShowReceive(false)}/>}

      <div className="card mb-2"><div className="card-body" style={{ padding: '10px 14px' }}>
        <div className="h-timeline">
          {stages.map((st, i) => {
            const current = !st.done && (i === 0 || stages[i - 1].done);
            return <div key={st.label} className={`h-step ${st.done ? 'done' : ''} ${current ? 'current' : ''}`}><Icon name={st.done ? 'check' : 'spinner'} size={11}/>{st.label}</div>;
          })}
        </div>
        <div className="tiny muted mt-1">Ordered → received → e-Billed → vendor invoice 3-way matched → booked for payment. Receiving auto-creates the e-Bill; a matching invoice auto-books.</div>
      </div></div>

      <ComboPOSummary po={po} getSO={getSO} getProduct={getProduct}/>

      {/* Client vs vendor billing — kept explicitly separate */}
      <div className="mb-2" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
        <div className="card" style={{ borderLeft: '3px solid var(--accent)' }}><div className="card-body">
          <div className="tiny muted" style={{ textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 600 }}>Vendor side · we pay (payable)</div>
          <div className="mono" style={{ fontSize: 17, fontWeight: 700, marginTop: 2 }}>{inr(grand)}</div>
          <div className="tiny muted">{featureOn('e_invoice') ? (ebilled ? <>e-Bill <span className="mono">{po.ebill.no}</span> · {fmtDate(po.ebill.date)} · </> : <>PO e-Bill not generated yet · </>) : null}{vInv ? `vendor invoice ${vInv.vendor_invoice_no} (3-way)` : 'no vendor invoice yet'}</div>
        </div></div>
        <div className="card"><div className="card-body">
          <div className="tiny muted" style={{ textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 600 }}>Client side · customer pays (receivable)</div>
          <div className="mono" style={{ fontSize: 17, fontWeight: 700, marginTop: 2 }}>{so && so.invoice_amount ? inr(so.invoice_amount) : '—'}</div>
          <div className="tiny muted">
            {so && so.invoice_no
              ? <>Tax invoice <span className="mono">{so.invoice_no}</span> · <a style={{ cursor: 'pointer', textDecoration: 'underline' }} onClick={() => navigate(`invoices/${so.id}`)}>view</a></>
              : 'Customer not invoiced yet — billed on the SO, separate from this PO'}
          </div>
        </div></div>
      </div>

      <div className="detail-grid">
        <div className="stack">
          <div className="card">
            <div className="card-header">
              <h3 className="card-title">PO Items</h3>
              {inVendorLang && <div className="tiny muted" title="Set on the Item Mapping screen">Printed in <strong>{v.name}</strong> part numbers{unmapped > 0 ? ` · ${unmapped} line(s) not mapped yet` : ' · all lines mapped'} · <a style={{ cursor: 'pointer', textDecoration: 'underline' }} onClick={() => navigate('mapping')}>edit mapping</a></div>}
            </div>
            <div className="card-body flush">
              <table className="t">
                <thead><tr><th>{inVendorLang ? `${v.name} part no. / description` : 'Product'}</th><th>{inVendorLang ? 'Our item' : 'HSN'}</th><th className="num">Qty</th><th className="num">Rate</th><th className="num">Amount</th></tr></thead>
                <tbody>
                  {po.items.map((it, i) => {
                    const p = getProduct(it.product_id);
                    const n = partyItemName(vendorAliases, p, poLang);
                    return (
                      <tr key={i}>
                        <td>
                          {n.name}
                          <div className="tiny muted mono">{n.code}</div>
                          {inVendorLang && !n.mapped && <div className="tiny" style={{ color: 'var(--warning)' }} title="No part number recorded for this vendor — our own name is printed instead">&#9888; not mapped to {v.name}</div>}
                        </td>
                        <td className="mono small">
                          {inVendorLang
                            ? <span title="Our catalogue">{n.altCode || p.code}<div className="tiny muted" style={{ fontFamily: 'inherit' }}>{n.altName || p.name}</div></span>
                            : p.hsn}
                        </td>
                        <td className="num">{qty(it.qty)} {(inVendorLang && n.uom) || p.uom}</td>
                        <td className="num">{inr(it.rate)}</td>
                        <td className="num">{inr(it.qty * it.rate)}</td>
                      </tr>
                    );
                  })}
                </tbody>
                <tfoot>
                  <tr><td colSpan="4" className="right small">Subtotal</td><td className="num">{inr(subtotal)}</td></tr>
                  {taxBreakdown.map(t => <tr key={t.key + '@' + t.rate}><td colSpan="4" className="right small">{t.label}</td><td className="num">{inr(t.amount)}</td></tr>)}
                  <tr><td colSpan="4" className="right"><strong>Grand Total</strong></td><td className="num"><strong>{inr(grand)}</strong></td></tr>
                </tfoot>
              </table>
            </div>
          </div>

          <div className="card">
            <div className="card-header"><h3 className="card-title">Terms & Conditions</h3></div>
            <div className="card-body small">
              <ul style={{ margin: 0, paddingLeft: 18, lineHeight: 1.7 }}>
                <li>Material to be delivered to {state.org.address} unless otherwise specified.</li>
                <li>Payment terms: {v.terms} from invoice date.</li>
                <li>Defective material to be replaced free of cost within 7 days of intimation.</li>
                <li>All disputes subject to Mumbai jurisdiction.</li>
              </ul>
            </div>
          </div>
        </div>

        <div className="stack">
          <div className="card">
            <div className="card-header"><h3 className="card-title">Vendor</h3></div>
            <div className="card-body">
              <div className="dl">
                <dt>Name</dt><dd>{v.name}</dd>
                <dt>GSTIN</dt><dd className="mono">{v.gstin}</dd>
                <dt>Contact</dt><dd>{v.contact} · {v.phone}</dd>
                <dt>City</dt><dd>{v.city}</dd>
                <dt>Rating</dt><dd>★ {v.rating}</dd>
                <dt>Terms</dt><dd>{v.terms}</dd>
              </div>
            </div>
          </div>
          <div className="card">
            <div className="card-header">
              <h3 className="card-title">Delivery</h3>
              {wfOn('intransit_tracking') && canProcurePO && !grn &&
                <button className="btn btn-sm" onClick={() => setShowTransit(true)}><Icon name="truck" size={12}/>{inTransit ? 'Update transit' : 'Mark dispatched'}</button>}
            </div>
            <div className="card-body">
              <div className="dl">
                <dt>Expected</dt><dd className="mono">{fmtDate(po.expected)}</dd>
                {wfOn('intransit_tracking') ? (
                  inTransit ? <>
                    <dt>Status</dt><dd>{grn ? <span className="badge success dot">Delivered · GRN posted</span> : <span className="badge accent dot">In transit</span>}</dd>
                    <dt>Dispatched</dt><dd className="mono">{dispatch.shipped_on ? fmtDate(dispatch.shipped_on) : '—'}</dd>
                    <dt>LR / docket</dt><dd className="mono">{dispatch.lr_no || '—'}</dd>
                    <dt>Carrier</dt><dd className="small">{dispatch.carrier || '—'}</dd>
                    {dispatch.tracking_url && <><dt>Track</dt><dd className="small"><a href={dispatch.tracking_url} target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'underline' }}>open tracking</a></dd></>}
                    {dispatch.contact && <><dt>Driver / contact</dt><dd className="mono small">{dispatch.contact}</dd></>}
                    <dt>ETA</dt><dd className="mono">{dispatch.eta ? fmtDate(dispatch.eta) : '—'}</dd>
                    {dispatch.notes && <><dt>Notes</dt><dd className="small">{dispatch.notes}</dd></>}
                  </> : <>
                    <dt>Status</dt><dd><span className="badge dot">Not dispatched yet</span></dd>
                  </>
                ) : <><dt>LR / Tracking</dt><dd className="mono">{dispatch.lr_no || '—'}</dd></>}
                <dt>Ship to</dt><dd className="small">{state.org.address}</dd>
              </div>
            </div>
          </div>

          {featureOn('e_invoice') && <div className="card">
            <div className="card-header"><h3 className="card-title">PO e-Bill</h3></div>
            <div className="card-body">
              {ebilled ? (
                <div className="dl">
                  <dt>e-Bill no.</dt><dd className="mono">{po.ebill.no}</dd>
                  <dt>IRN</dt><dd className="mono small">{po.ebill.irn}</dd>
                  <dt>Generated</dt><dd className="mono">{fmtDate(po.ebill.date)}</dd>
                  <dt>Amount</dt><dd className="mono">{inr(po.ebill.amount || grand)}</dd>
                </div>
              ) : (
                <div className="small muted">No e-Bill yet. Generate it to issue an official PO document, stored on this Vendor PO. <div className="mt-2"><button className="btn btn-sm btn-primary" onClick={genEbill}><Icon name="receipt" size={12}/>Generate PO e-Bill</button></div></div>
              )}
              {ebilled && <div className="mt-2" style={{ display: 'flex', gap: 6 }}>
                <button className="btn btn-sm btn-primary" onClick={() => setShowEbill(true)}><Icon name="receipt" size={12}/>View</button>
                <button className="btn btn-sm" onClick={() => printPOEbill(po, v, so, state.org, getProduct)}><Icon name="print" size={12}/>Print</button>
              </div>}
            </div>
          </div>}

          <div className="card">
            <div className="card-header"><h3 className="card-title">Project vendor history</h3></div>
            <div className="card-body flush">
              <div className="small muted" style={{ padding: '6px 14px' }}>All vendors selected for <span className="mono">{so?.so_no}</span></div>
              <table className="t">
                <thead><tr><th>PO</th><th>Vendor</th><th className="num">Amount</th><th>Status</th></tr></thead>
                <tbody>
                  {siblings.map(s => {
                    const sv = getVendor(s.vendor_id);
                    return (
                      <tr key={s.id} className={s.id === po.id ? 'selected' : ''} onClick={() => navigate(`vendor-pos/${s.id}`)} style={{ cursor: 'pointer' }}>
                        <td><a className="mono">{s.po_no}</a></td>
                        <td className="small">{sv?.name}</td>
                        <td className="num">{inr(s.amount)}</td>
                        <td>{s.ebill && s.ebill.generated ? <span className="badge success dot">e-Bill</span> : <span className="badge dot">{s.status}</span>}</td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// ===== GRN =====
function GRNList() {
  const { state, navigate, getVendor, getSO, getCustomer, getProduct, getUser, mutate, addToPool, consumeFromPool, currentUser } = useStore();
  const toast = useToast();
  const role = (getUser(currentUser) || {}).role;
  // Per-org: standard = Stores accepts what Purchase marked;
  // procurement-only = Purchase accepts what Stores confirmed.
  const flow = wfReceiving();
  const canAccept = flow.approverRoles.includes(role);
  const [busy, setBusy] = React.useState('');
  const [viewRec, setViewRec] = React.useState(null);
  // Grouped by SO, same as Vendor POs (screens-procurement.jsx, VendorPOList)
  // — one project's GRNs were scattered across a single flat list with
  // nothing to tell them apart at a glance. Only this bottom table changes;
  // the pending-receipts / Master-Pool panels above are untouched.
  const [grnSearch, setGrnSearch] = React.useState('');
  const [grnVendorF, setGrnVendorF] = React.useState('');
  const [grnGroupBySO, setGrnGroupBySO] = React.useState(true);
  // Purchase's Master-Pool requests (add-from / send-to) awaiting Stores' approval.
  const poolPending = [];
  (state.sales_orders || []).forEach(so => ((so.extra && so.extra.pool_requests) || []).forEach(req => { if (req.status === 'Pending') poolPending.push({ so, req }); }));
  // All completed pool movements (proof records) — shown here so they live "in GRN" too.
  const poolReceipts = [];
  (state.sales_orders || []).forEach(so => ((so.extra && so.extra.pool_receipts) || []).forEach(rec => poolReceipts.push({ so, rec })));
  poolReceipts.sort((a, b) => (b.rec.no || '').localeCompare(a.rec.no || ''));

  const acceptPool = async ({ so, req }) => {
    setBusy(req.id);
    const meta = { requested_by: req.by, accepted_by: currentUser, by: currentUser };
    const ctx = { state, mutate, consumeFromPool, addToPool, getProduct, currentUser, meta };
    let ok = true;
    if (req.kind === 'add') {
      for (const it of (req.items || [])) { const r = await window.poolAllocateToSO(so, { product_id: it.product_id, qty: it.qty }, ctx); if (!r || !r.ok) ok = false; }
    } else {
      const r = await window.poolSendFromSO(so, req.items || [], ctx); if (!r || !r.ok) ok = false;
    }
    mutate(s => ({
      ...s,
      sales_orders: s.sales_orders.map(x => x.id === so.id ? { ...x, extra: { ...(x.extra || {}), pool_requests: ((x.extra && x.extra.pool_requests) || []).map(p => p.id === req.id ? { ...p, status: 'Accepted', accepted_by: currentUser, accepted_date: TODAY } : p) } } : x),
      notifications: [{ id: 'n-pla-' + Date.now(), kind: 'transfer', text: `${so.so_no}: Stores approved your Master-Pool ${req.kind === 'add' ? 'pull' : 'send'} · ${(req.items || []).map(it => `${it.qty}× ${it.name}`).join(', ')}`, date: TODAY, read: false, user_id: req.by }, ...s.notifications],
    }), { action: 'pool-approve', entity: 'SalesOrder', entity_id: so.id, user_id: currentUser, detail: `Stores approved pool ${req.kind} · ${so.so_no} · ${(req.items || []).map(it => `${it.qty}× ${it.name}`).join(', ')}` });
    setBusy('');
    toast(ok ? `Approved · pool ${req.kind === 'add' ? 'items added to ' + so.so_no : 'items sent to pool'} · receipt saved` : 'Approved, but some quantities were unavailable — please re-check', ok ? 'success' : '');
  };
  const rejectPool = ({ so, req }) => {
    mutate(s => ({
      ...s,
      sales_orders: s.sales_orders.map(x => x.id === so.id ? { ...x, extra: { ...(x.extra || {}), pool_requests: ((x.extra && x.extra.pool_requests) || []).map(p => p.id === req.id ? { ...p, status: 'Rejected', rejected_by: currentUser, rejected_date: TODAY } : p) } } : x),
      notifications: [{ id: 'n-plrj-' + Date.now(), kind: 'transfer', text: `${so.so_no}: Stores declined your Master-Pool ${req.kind === 'add' ? 'pull' : 'send'} request`, date: TODAY, read: false, user_id: req.by }, ...s.notifications],
    }), { action: 'pool-reject', entity: 'SalesOrder', entity_id: so.id, user_id: currentUser, detail: `Stores declined pool ${req.kind} · ${so.so_no}` });
    toast('Request returned to Purchase', '');
  };

  return (
    <div className="page">
      <div className="page-header">
        <div>
          <h1 className="page-title">Goods Receipt Notes (GRN)</h1>
          <div className="page-sub">{flow.note} · auto-allocated to the SO's Virtual Godown</div>
        </div>
        <div className="page-actions">
          <span className="tiny muted" style={{ display: 'flex', alignItems: 'center', gap: 6 }}><Icon name="check" size={12} color="var(--success)"/>Auto-created on receipt at the Virtual Godown</span>
          <button className="btn" onClick={() => navigate('godown')}><Icon name="box" size={13}/>Go to Virtual Godowns</button>
          <button className="btn btn-primary" onClick={() => navigate('grn/new')}><Icon name="plus" size={13}/>New GRN</button>
        </div>
      </div>

      {/* Same records and the same actions as the order's Virtual Godown and its
          Vendor POs tab — one implementation, shown wherever the work is. */}
      <div style={{ marginBottom: 14 }}><PendingReceiptsPanel/></div>

      {poolPending.length > 0 && (
        <div className="card" style={{ marginBottom: 14, borderColor: 'var(--accent)' }}>
          <div className="card-header"><div className="card-title" style={{ display: 'flex', alignItems: 'center', gap: 8 }}><Icon name="layers" size={14} color="var(--accent)"/>Master-Pool requests awaiting Stores <span className="badge accent dot">{poolPending.length}</span></div><div className="tiny muted">Purchase asked to move stock to/from the Master Pool — approve to execute &amp; record the receipt</div></div>
          <div className="card-body flush">
            <table className="t">
              <thead><tr><th>Sales Order</th><th>Direction</th><th>Requested by</th><th>Date</th><th>Items</th><th></th></tr></thead>
              <tbody>
                {poolPending.map(({ so, req }) => (
                  <tr key={req.id}>
                    <td><a className="mono" onClick={() => navigate(`godown/${so.id}`)} style={{ cursor: 'pointer' }}>{so.so_no}</a></td>
                    <td>{req.kind === 'add' ? <span className="badge success dot">From pool</span> : <span className="badge info dot">To pool</span>}</td>
                    <td className="small">{(getUser(req.by) || {}).name || req.by}</td>
                    <td className="mono small">{fmtDate(req.date)}</td>
                    <td className="small">{(req.items || []).map(it => `${it.qty}× ${it.name}`).join(', ')}</td>
                    <td style={{ textAlign: 'right', whiteSpace: 'nowrap' }}>
                      {canAccept ? (
                        <>
                          <button className="btn sm primary" disabled={busy === req.id} onClick={() => acceptPool({ so, req })}>{busy === req.id ? 'Applying…' : 'Approve'}</button>
                          <button className="btn sm ghost" disabled={busy === req.id} onClick={() => rejectPool({ so, req })} style={{ marginLeft: 6 }}>Reject</button>
                        </>
                      ) : <span className="tiny muted">awaiting Stores</span>}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      )}

      {poolReceipts.length > 0 && (
        <div className="card" style={{ marginBottom: 14 }}>
          <div className="card-header"><div className="card-title" style={{ display: 'flex', alignItems: 'center', gap: 8 }}><Icon name="layers" size={13}/>Master-Pool movement receipts</div><div className="tiny muted">{poolReceipts.length} record(s) · proof of every add-from / send-to Master Pool</div></div>
          <div className="card-body flush">
            <table className="t">
              <thead><tr><th>Receipt</th><th>Sales Order</th><th>Direction</th><th>Items</th><th>By</th><th>Date</th></tr></thead>
              <tbody>
                {poolReceipts.slice(0, 50).map(({ so, rec }) => (
                  <tr key={rec.id} onClick={() => setViewRec({ rec, so })} style={{ cursor: 'pointer' }}>
                    <td className="mono small"><a>{rec.no}</a></td>
                    <td className="mono small">{rec.so_no || so.so_no}</td>
                    <td>{rec.direction === 'in' ? <span className="badge success dot">From pool</span> : <span className="badge info dot">To pool</span>}</td>
                    <td className="small">{(rec.items || []).map(it => `${it.qty}× ${it.name}`).join(', ')}</td>
                    <td className="small">{(getUser(rec.accepted_by || rec.by) || {}).name || rec.by}</td>
                    <td className="mono small">{fmtDate(rec.date)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      )}
      {viewRec && <PoolReceiptModal receipt={viewRec.rec} so={viewRec.so} onClose={() => setViewRec(null)}/>}

      <div className="card">
        <div className="filter-bar">
          <input className="input search" placeholder="Search GRN no, vendor PO, vendor, SO, customer, LR…"
            value={grnSearch} onChange={e => setGrnSearch(e.target.value)} style={{ flex: '0 0 260px' }}/>
          <select className="select" style={{ width: 150 }} value={grnVendorF} onChange={e => setGrnVendorF(e.target.value)}>
            <option value="">All vendors</option>
            {state.vendors.map(v => <option key={v.id} value={v.id}>{v.name}</option>)}
          </select>
          <div className="grow"/>
          <button className={`btn ${grnGroupBySO ? 'btn-primary' : ''}`} onClick={() => setGrnGroupBySO(g => !g)}>
            <Icon name="layers" size={13}/>{grnGroupBySO ? 'Grouped by project' : 'Flat list'}</button>
        </div>

        {(() => {
          const grnRows = state.grns.filter(g => {
            const po = state.vendor_pos.find(p => p.id === g.po_id);
            if (grnVendorF && (!po || po.vendor_id !== grnVendorF)) return false;
            if (grnSearch) {
              const v = po && getVendor(po.vendor_id);
              const so = po && getSO(po.so_id);
              const cust = so && getCustomer(so.customer_id);
              const blob = `${g.grn_no} ${po?.po_no || ''} ${v?.name || ''} ${so?.so_no || ''} ${cust?.name || ''} ${g.lr || ''}`.toLowerCase();
              if (!blob.includes(grnSearch.toLowerCase())) return false;
            }
            return true;
          });

          const Row = (g) => {
            const po = state.vendor_pos.find(p => p.id === g.po_id);
            const v = po && getVendor(po.vendor_id);
            const so = po && getSO(po.so_id);
            const cust = so && getCustomer(so.customer_id);
            return (
              <tr key={g.id} onClick={() => navigate(`grn/${g.id}`)} style={{ cursor: 'pointer' }}>
                <td><a className="mono">{g.grn_no}</a></td>
                <td className="mono">{po?.po_no}</td>
                <td>{v ? v.name : '—'}</td>
                {!grnGroupBySO && <td className="mono small">{so?.so_no}<div className="tiny muted">{cust?.name}</div></td>}
                <td className="mono small">{fmtDate(g.date)}</td>
                <td className="mono small">{g.lr}</td>
                <td className="num">{g.items.length}</td>
                <td><span className="badge success dot">Posted</span></td>
                <td><Icon name="chevronRight" size={12}/></td>
              </tr>
            );
          };

          if (state.grns.length === 0) {
            return <div className="card-body"><div className="empty">No GRNs yet — they appear here once material is received against a Vendor PO.</div></div>;
          }
          if (grnRows.length === 0) {
            return <div className="card-body"><div className="empty">No GRNs match your search.</div></div>;
          }
          if (!grnGroupBySO) {
            return (
              <div className="table-wrap"><table className="t"><thead><tr>
                <th>GRN No</th><th>Vendor PO</th><th>Vendor</th><th>For SO · Customer</th><th>Received Date</th><th>LR No</th>
                <th className="num">Lines</th><th>Status</th><th></th>
              </tr></thead><tbody>{grnRows.map(Row)}</tbody></table></div>
            );
          }
          // Grouped by SO (project) — same layout VendorPOList already uses,
          // so a project's receipts and its vendor POs read the same way.
          const groups = {};
          grnRows.forEach(g => {
            const po = state.vendor_pos.find(p => p.id === g.po_id);
            const key = (po && po.so_id) || '—';
            (groups[key] = groups[key] || []).push(g);
          });
          return Object.entries(groups).map(([soId, grns]) => {
            const so = soId !== '—' ? getSO(soId) : null;
            const cust = so ? getCustomer(so.customer_id) : null;
            const totalLines = grns.reduce((s, g) => s + g.items.length, 0);
            return (
              <div key={soId} style={{ borderTop: '1px solid var(--border)' }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 14px', background: 'var(--bg-subtle, var(--surface))' }}>
                  <div className="small">
                    <strong className="mono" onClick={() => so && navigate(`sales-orders/${soId}`)} style={{ cursor: so ? 'pointer' : 'default' }}>{so ? so.so_no : 'Unlinked'}</strong>
                    {cust && <span className="muted"> · {cust.name}</span>} <span className="muted">· {grns.length} GRN(s)</span>
                  </div>
                  <div className="small mono">{totalLines} line(s)</div>
                </div>
                <div className="table-wrap"><table className="t"><thead><tr>
                  <th>GRN No</th><th>Vendor PO</th><th>Vendor</th><th>Received Date</th><th>LR No</th>
                  <th className="num">Lines</th><th>Status</th><th></th>
                </tr></thead><tbody>{grns.map(Row)}</tbody></table></div>
              </div>
            );
          });
        })()}
      </div>
    </div>
  );
}

function GRNDetail({ grnId }) {
  const { state, navigate, getVendor, getProduct } = useStore();
  const g = state.grns.find(x => x.id === grnId);
  if (!g) return <div className="page"><div className="empty">{state.loaded ? 'GRN not found' : 'Loading…'}</div></div>;
  const po = state.vendor_pos.find(p => p.id === g.po_id);
  const v = getVendor(po.vendor_id);

  return (
    <div className="page">
      <div className="page-header">
        <div>
          <div className="muted tiny mb-1" style={{ cursor: 'pointer' }} onClick={() => navigate('grn')}>
            <Icon name="chevronLeft" size={12}/> GRN
          </div>
          <h1 className="page-title"><span className="mono">{g.grn_no}</span> <span className="badge success dot ml-2" style={{ marginLeft: 8 }}>Posted</span></h1>
          <div className="page-sub">For VPO <span className="mono">{po.po_no}</span> · {v.name} · received {fmtDate(g.date)}</div>
        </div>
        <div className="page-actions">
        </div>
      </div>

      <div className="detail-grid">
        <div className="stack">
          <div className="card">
            <div className="card-header"><h3 className="card-title">Receipt Lines</h3><span className="card-sub">Status reflects cumulative receipts against this PO</span></div>
            <div className="card-body flush">
              <table className="t zebra">
                <thead><tr><th>Item</th><th className="num">Ordered</th><th className="num">Received</th><th className="num">Accepted</th><th className="num">Rejected</th><th className="num">Remaining</th><th>Status</th></tr></thead>
                <tbody>
                  {g.items.map((it, i) => {
                    const p = getProduct(it.product_id);
                    // Cumulative accepted for this product across ALL GRNs of the PO → live remaining.
                    const cumAcc = state.grns.filter(x => x.po_id === g.po_id).reduce((a, x) => a + (x.items || []).filter(y => y.product_id === it.product_id).reduce((b, y) => b + (y.accepted || 0), 0), 0);
                    const ordered = (po.items || []).filter(y => y.product_id === it.product_id).reduce((a, y) => a + (y.qty || 0), 0) || it.ordered || 0;
                    const remaining = Math.max(0, ordered - cumAcc);
                    return (
                      <tr key={i}>
                        <td>{p.name}<div className="tiny muted mono">{p.code}</div></td>
                        <td className="num">{it.ordered}</td>
                        <td className="num">{it.received}</td>
                        <td className="num"><span style={{ color: 'var(--success)' }}>{it.accepted}</span></td>
                        <td className="num">{it.rejected || <span className="muted">0</span>}</td>
                        <td className="num">{remaining > 0 ? <strong style={{ color: 'var(--warning)' }}>{remaining}</strong> : <span className="muted">0</span>}</td>
                        <td>{remaining > 0 ? <span className="badge warning dot">Remaining</span> : <span className="badge success dot">Fulfilled</span>}</td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          </div>

          <div className="card">
            <div className="card-header"><h3 className="card-title">QC Checklist</h3></div>
            <div className="card-body">
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 6 }}>
                {[
                  ['Outer packaging intact', true],
                  ['Quantity matches PO', true],
                  ['Serial numbers recorded', true],
                  ['Visual inspection passed', true],
                  ['Documentation (LR, invoice) attached', true],
                  ['Damage report (if any)', false],
                ].map(([label, ok], i) => (
                  <div key={i} className="small" style={{ display: 'flex', alignItems: 'center', gap: 6, padding: 6, border: '1px solid var(--border)', borderRadius: 4 }}>
                    <Icon name={ok ? 'check' : 'x'} size={13} color={ok ? 'var(--success)' : 'var(--text-3)'}/>
                    {label}
                  </div>
                ))}
              </div>
            </div>
          </div>
        </div>

        <div className="stack">
          <div className="card">
            <div className="card-header"><h3 className="card-title">Receipt Info</h3></div>
            <div className="card-body">
              <div className="dl">
                <dt>Vendor</dt><dd>{v.name}</dd>
                <dt>LR / tracking</dt><dd className="mono">{g.lr}</dd>
                <dt>Received by</dt><dd>Arun Bhatia</dd>
                <dt>Receipt date</dt><dd className="mono">{fmtDate(g.date)}</dd>
              </div>
            </div>
          </div>
          <div className="card">
            <div className="card-header"><h3 className="card-title">Auto-routed to VG</h3></div>
            <div className="card-body">
              <div className="small muted mb-2">Items added to Virtual Godown of:</div>
              <div className="pool-item">
                <div>
                  <div style={{ fontWeight: 500, fontSize: 12.5 }} className="mono">{state.sales_orders.find(s => s.id === po.so_id)?.so_no}</div>
                  <div className="tiny muted">23 components added</div>
                </div>
                <button className="btn btn-sm" onClick={() => navigate(`godown/${po.so_id}`)}>View VG</button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// Shared receipt poster — used by the single-PO GRN screen AND the per-vendor
// Receive modal. Creates the GRN, auto-stamps the PO e-Bill, auto-books the
// vendor (payable) invoice for the received value, routes surplus to the Master
// Pool, auto-reduces the client bill for removed items, and auto-raises the
// client partial invoice. items: [{product_id, qty(ordered), received, rejected, reason, to_pool}].
// One e-Bill number per PO, derived from the PO's own number so it cannot
// collide with another issued in the same breath.
function poEbillNo(po) {
  // The e-Bill and the vendor's invoice are the same document number for the
  // same purchase order: PO202609001 -> INV202609001. One number to quote when
  // anybody asks about that PO.
  return vendorInvoiceNo((po && po.po_no) || (po && po.id) || '');
}
window.poEbillNo = poEbillNo;

async function postReceiptForPO(po, items, meta, ctx) {
  const { state, mutate, toast, addToPool, getProduct, getVendor, currentUser, getUser } = ctx;
  const grnDate = (meta && meta.grnDate) || TODAY; const lr = (meta && meta.lr) || '';
  const seqOff = (meta && meta.seqOffset) || 0;   // keeps numbering unique when posting several POs in one action
  const norm = items.map(it => ({ ...it, accepted: Math.max(0, (it.received || 0) - (it.rejected || 0) - (it.to_pool || 0)) }));
  const surplus = norm.filter(it => (it.to_pool || 0) > 0).map(it => ({ product_id: it.product_id, qty: it.to_pool, source_so: po.so_id, received_date: grnDate }));
  const surplusUnits = surplus.reduce((s, x) => s + x.qty, 0);
  const grnNo = `GRN/FY26/${String(28 + state.grns.length + seqOff).padStart(4, '0')}`;
  const rnd = Math.random().toString(36).slice(2, 5);
  const grn = {
    id: 'grn-' + Date.now() + rnd, grn_no: grnNo, po_id: po.id, date: grnDate, lr, received_by: 'Stores', status: 'Posted',
    items: norm.map(it => ({ product_id: it.product_id, ordered: it.qty, received: it.received, accepted: it.accepted, rejected: it.rejected || 0, reject_reason: it.reason || null, to_pool: it.to_pool || 0,
      ...(Array.isArray(it.so_split) && it.so_split.length ? { so_split: it.so_split } : {}) })),
  };
  const adjustments = norm.filter(it => (it.to_pool || 0) > 0).map(it => { const p = getProduct(it.product_id); return { product_id: it.product_id, qty: it.to_pool, amount: Math.round((p ? (p.sell || 0) : 0) * it.to_pool), reason: 'Removed at GRN — not supplied to customer', grn_id: grn.id, date: grnDate }; });
  const billCut = adjustments.reduce((s, a) => s + a.amount, 0);
  const ebill = po.ebill && po.ebill.generated ? po.ebill : { no: poEbillNo(po), irn: (po.id + po.po_no).replace(/[^a-zA-Z0-9]/g, '').slice(0, 16).toUpperCase(), date: grnDate, amount: Math.round((po.amount || 0) * 1.18), generated: true, auto: true };
  const rateOf = {}; (po.items || []).forEach(it => { rateOf[it.product_id] = it.rate; });
  const recvValue = norm.reduce((s, it) => s + (it.accepted || 0) * (rateOf[it.product_id] || 0), 0);
  const autoVI = recvValue > 0 ? { id: 'vi-' + Date.now() + rnd, vendor_invoice_no: vendorInvoiceNo(po.po_no), po_id: po.id, grn_id: grn.id, vendor_id: po.vendor_id, date: grnDate, amount: Math.round(recvValue), status: 'Booked', tolerance: 'within' } : null;
  // A PO is only fully received when cumulative accepted >= ordered for EVERY line
  // (prior GRNs for this PO + this receipt). Otherwise it stays 'Partially Received'
  // so it remains receivable for the rest. Whole-PO receipts still resolve to complete.
  const cumAcc = {};
  state.grns.filter(g => g.po_id === po.id).forEach(g => (g.items || []).forEach(it => { cumAcc[it.product_id] = (cumAcc[it.product_id] || 0) + (it.accepted || 0); }));
  norm.forEach(it => { cumAcc[it.product_id] = (cumAcc[it.product_id] || 0) + (it.accepted || 0); });
  const poComplete = (po.items || []).every(it => (cumAcc[it.product_id] || 0) >= (it.qty || 0));
  const newPoStatus = poComplete ? 'Material Received' : 'Partially Received';
  const so = state.sales_orders.find(x => x.id === po.so_id);
  mutate(s => ({
    ...s,
    grns: [grn, ...s.grns],
    vendor_pos: s.vendor_pos.map(p => p.id === po.id ? { ...p, status: newPoStatus, ebill } : p),
    vendor_invoices: autoVI ? [autoVI, ...s.vendor_invoices] : s.vendor_invoices,
    sales_orders: (po.so_id && adjustments.length) ? s.sales_orders.map(x => x.id === po.so_id ? { ...x, bill_adjustments: [...(x.bill_adjustments || []), ...adjustments] } : x) : s.sales_orders,
    notifications: [
      { id: 'n-grn-' + Date.now() + rnd, kind: 'grn', text: `${grnNo} posted for ${po.po_no}${so ? ' · ' + so.so_no : ''} · received + e-Bill auto-saved`, date: TODAY, read: false, role: 'Billing' },
      ...(autoVI ? [{ id: 'n-payv-' + Date.now() + rnd, kind: 'match', text: `${autoVI.vendor_invoice_no} auto-booked · ${inrK(autoVI.amount)} payable to ${getVendor(po.vendor_id)?.name || 'vendor'} (${po.po_no})`, date: TODAY, read: false, role: 'Billing' }] : []),
      ...(surplusUnits ? [{ id: 'n-pool-' + Date.now() + rnd, kind: 'transfer', text: `${surplusUnits} surplus unit(s) → Master Pool from ${po.po_no}`, date: TODAY, read: false, role: 'Stores' }] : []),
      ...(billCut ? [{ id: 'n-bill-' + Date.now() + rnd, kind: 'so', text: `${so ? so.so_no : po.po_no}: customer bill auto-reduced by ${inrK(billCut)} (items not supplied)`, date: TODAY, read: false, role: 'Billing' }] : []),
    ],
  }), { action: 'create', entity: 'GRN', entity_id: grn.id });
  if (surplus.length) await addToPool(surplus);
  // Skip the per-PO client invoice when the caller will raise one consolidated
  // invoice after posting several POs (VG "Mark Received"). Default behaviour
  // (single-PO receive) is unchanged.
  if (!(meta && meta.skipInvoice) && po.so_id && window.autoInvoiceSO) window.autoInvoiceSO(po.so_id, { mutate, toast: null, currentUser, getUser, getProduct });
  // A PO combined across several SOs also invoices every OTHER linked SO that
  // received units in THIS GRN event — each gets billed only its own slice
  // (soReceivedQty/soInvoiceState already read grn.items by so_split), never
  // the whole receipt. No-op for the overwhelming majority of POs, which
  // serve only po.so_id, already invoiced above.
  if (!(meta && meta.skipInvoice) && window.autoInvoiceSO) {
    poLinkedSoIds(po).filter(soId => soId !== po.so_id).forEach(soId => {
      if (grn.items.some(it => grnLineSoQty(it, po, soId) > 0)) {
        window.autoInvoiceSO(soId, { mutate, toast: null, currentUser, getUser, getProduct });
      }
    });
  }
  return { grn, surplusUnits, billCut, poComplete };
}
window.postReceiptForPO = postReceiptForPO;

// The PO-centric receive dialogs (ReceiveModal, GRNNew) used to call
// postReceiptForPO directly, no matter who clicked Receive — so a requester
// role (Stores, when receiving_flow is stores_to_purchase) could post a GRN
// straight through, and the request Purchase's GRN dashboard is supposed to
// show never got raised at all. wfReceiving() already gates VG's own "mark
// received"; this is the SAME gate, applied here too, so the setting
// actually governs every receiving door, not just one of them.
//
// The acceptor posts directly, unchanged. Anyone else's exact items —
// rejected / to_pool / so_split already decided in the dialog — are stashed
// as a pending request on the PO's own SO, replayed verbatim by
// PendingReceiptsPanel's Accept (screens-godown.jsx) later. Nothing here
// re-derives what was already decided.
async function submitPOReceipt(po, items, meta, ctx) {
  const { state, mutate, getUser, currentUser } = ctx;
  const role = (getUser(currentUser) || {}).role;
  const flow = wfReceiving();
  if (flow.approverRoles.includes(role)) {
    return { posted: true, result: await postReceiptForPO(po, items, meta, ctx) };
  }
  const units = items.reduce((a, it) => a + Math.max(0, (Number(it.received) || 0) - (Number(it.rejected) || 0) - (Number(it.to_pool) || 0)), 0);
  const so = state.sales_orders.find(x => x.id === po.so_id);
  const pr = { id: 'pr-' + Date.now(), by: currentUser, date: TODAY, status: 'Pending', flow: flow.mode,
    po_id: po.id, items, lr: (meta && meta.lr) || '', grnDate: (meta && meta.grnDate) || TODAY };
  mutate(s => ({
    ...s,
    sales_orders: s.sales_orders.map(x => x.id === po.so_id ? { ...x, extra: { ...(x.extra || {}), pending_receipts: [...((x.extra && x.extra.pending_receipts) || []), pr] } } : x),
    notifications: [{ id: 'n-recq-' + Date.now(), kind: 'grn', text: `${po.po_no}${so ? ' · ' + so.so_no : ''}: ${units} unit(s) confirmed by ${role} · ${flow.approverLabel} to accept & post GRN`, date: TODAY, read: false, role: flow.approverLabel }, ...s.notifications],
  }), { action: 'receipt-request', entity: 'VendorPO', entity_id: po.id, user_id: currentUser, detail: `Receipt sent to ${flow.approverLabel} · ${po.po_no} · ${units} unit(s)` });
  return { posted: false, units };
}
window.submitPOReceipt = submitPOReceipt;

// ===== Suggesting how a shared PO line's receipt splits across its SOs =====
// A SUGGESTION only — GRN can override every number before submitting; this
// never writes anything itself. Same greedy, ranked-allocation idiom
// soRejectedOutstanding/allocBuildRows already use elsewhere in this app, not
// an invented solver: rank the SOs sharing this line by how urgently they
// need it, then hand each one what it needs, in that order, until the
// accepted quantity runs out.
//
//   allocations: [{ so_id, orderedQty, alreadyReceived }] — one entry per SO
//     this line is shared with (so_id must include every linked SO, even
//     ones already fully received — they are dropped here, not by the caller,
//     so "fully received" is answered in exactly one place).
//   getSO(so_id) -> { so_no, priority, expected } | null
//
// Rank: Critical > Urgent > Standard (the SAME two literal values
// NewTransferModal already filters stock-lending destinations on — not a new
// per-tenant hardcode), then expected date ascending, then so_no ascending —
// fully deterministic, so re-running on the same facts always gives the same
// suggestion.
function suggestSoSplit(accepted, allocations, getSO) {
  const PRIO_RANK = { Critical: 0, Urgent: 1, Standard: 2 };
  const rows = (allocations || [])
    .map(a => {
      const remaining = Math.max(0, (Number(a.orderedQty) || 0) - (Number(a.alreadyReceived) || 0));
      const so = getSO ? getSO(a.so_id) : null;
      return { so_id: a.so_id, so_no: (so && so.so_no) || a.so_id, priority: (so && so.priority) || 'Standard',
               expected: (so && so.expected) || '', remaining };
    })
    .filter(r => r.remaining > 0.0001)
    .sort((x, y) =>
      (PRIO_RANK[x.priority] ?? 2) - (PRIO_RANK[y.priority] ?? 2)
      || (x.expected || '9999-99-99').localeCompare(y.expected || '9999-99-99')
      || x.so_no.localeCompare(y.so_no));

  let pool = Math.max(0, Number(accepted) || 0);
  rows.forEach(r => {
    const give = Math.min(r.remaining, pool);
    r.suggested = give;
    pool -= give;
    r.reason = `${r.so_no} — ${r.priority}, ${r.expected ? 'expected ' + r.expected : 'no delivery date set'} → suggested ${give}`;
  });
  return { rows, leftover: pool };
}
window.suggestSoSplit = suggestSoSplit;

// Build suggestSoSplit's own `allocations` input for one PO line, straight
// from app state — every linked SO's own ordered share and what it has
// already received on EARLIER GRNs against this same PO (so a second,
// partial delivery only offers what is genuinely still outstanding).
//
// poLinkedSoIds(po) is PO-wide — every SO sharing ANY line, not just this
// one. A PO with two independently-shared lines (so_alloc on line A has
// {X,Y}, so_alloc on line B has {X,W}) would otherwise hand line B's split
// editor a row for Y with "needs 0" even though Y has nothing to do with
// line B. Dropped here, at the source, rather than filtered by each of
// SoSplitEditor/LinePoSplitEditor separately — a SO with neither an order
// nor a receipt against THIS product never belonged in THIS line's answer.
function poSoAllocations(state, po, productId) {
  return poLinkedSoIds(po).map(so_id => {
    const orderedQty = poLineSoQty(po, productId, so_id);
    let alreadyReceived = 0;
    (state.grns || []).forEach(g => { if (g.po_id === po.id) (g.items || []).forEach(it => {
      if (it.product_id === productId) alreadyReceived += grnLineSoQty(it, po, so_id);
    }); });
    return { so_id, orderedQty, alreadyReceived };
  }).filter(a => a.orderedQty > 0.0001 || a.alreadyReceived > 0.0001);
}
window.poSoAllocations = poSoAllocations;

// GRN-time control for a shared line: full manual override, with a one-click
// "use suggested" fill from suggestSoSplit. Never auto-commits — the caller
// only gets `value` when the person submits the receipt.
function SoSplitEditor({ po, productId, accepted, value, onChange }) {
  const { state, getSO } = useStore();
  const allocations = poSoAllocations(state, po, productId);
  const rowQty = (soId) => { const r = (value || []).find(x => x.so_id === soId); return r ? (Number(r.qty) || 0) : 0; };
  const total = (value || []).reduce((s, r) => s + (Number(r.qty) || 0), 0);
  const balanced = Math.abs(total - (Number(accepted) || 0)) < 0.0001;
  const setQty = (soId, qty) => {
    const q = Math.max(0, Number(qty) || 0);
    onChange(allocations.map(a => ({ so_id: a.so_id, qty: a.so_id === soId ? q : rowQty(a.so_id) })));
  };
  const useSuggested = () => {
    const { rows } = suggestSoSplit(accepted, allocations, getSO);
    onChange(allocations.map(a => { const s = rows.find(x => x.so_id === a.so_id); return { so_id: a.so_id, qty: s ? s.suggested : 0 }; }));
  };
  return (
    <div style={{ padding: '6px 8px', background: 'var(--bg-subtle)', borderRadius: 4, marginTop: 4 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4, flexWrap: 'wrap' }}>
        <span className="tiny muted">Shared across {allocations.length} SO(s) — split the {accepted} accepted:</span>
        <button type="button" className="btn btn-ghost btn-sm" onClick={useSuggested}><Icon name="sparkles" size={11}/>Use suggested split</button>
        <span className="tiny mono" style={{ marginLeft: 'auto', color: balanced ? 'var(--success)' : 'var(--danger)' }}>{total} / {accepted}</span>
      </div>
      {allocations.map(a => { const so = getSO(a.so_id); const remaining = Math.max(0, a.orderedQty - a.alreadyReceived); return (
        <div key={a.so_id} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '2px 0' }}>
          <span className="tiny mono" style={{ width: 130 }}>{so ? so.so_no : a.so_id}</span>
          <span className="tiny muted" style={{ width: 90 }}>needs {remaining}</span>
          <input type="number" min="0" className="input mono" value={rowQty(a.so_id)} onChange={e => setQty(a.so_id, e.target.value)} style={{ width: 64, height: 24, textAlign: 'right' }}/>
        </div>
      ); })}
    </div>
  );
}
window.SoSplitEditor = SoSplitEditor;

// Editing an ALREADY-ISSUED PO line's SO split (Moment 3 — "which SO is this
// extra qty for?"). Candidates are every other open SO still outstanding for
// this item (soOutstandingProcurement — the same discovery CreateVendorPOModal
// uses) PLUS whoever is already on this line's so_alloc, shown even at zero
// outstanding since this very PO is what already covers them.
//
// A member already GRN'd on this PO can never be edited below what it has
// actually received — "can't un-receive", the same rule this screen already
// follows for a received PO as a whole.
function LinePoSplitEditor({ state, po, it, onSave, onCancel }) {
  // Same "how much has each linked SO already received on this exact line"
  // poSoAllocations already answers for SoSplitEditor — one definition, not
  // two independently-maintained loops that could silently drift apart.
  const already = {};
  poSoAllocations(state, po, it.product_id).forEach(a => { already[a.so_id] = a.alreadyReceived; });
  const linkedIds = (it.so_alloc && it.so_alloc.length ? it.so_alloc.map(a => a.so_id) : [po.so_id]);
  const otherOpen = (state.sales_orders || []).filter(s => s.id !== po.so_id && linkedIds.indexOf(s.id) === -1 && ['Closed', 'Cancelled'].indexOf(s.status) === -1);
  const otherOutstanding = {};
  otherOpen.forEach(os => { otherOutstanding[os.id] = (window.soOutstandingProcurement ? window.soOutstandingProcurement(state, os)[it.product_id] : 0) || 0; });
  const candidateIds = Array.from(new Set([
    po.so_id, ...linkedIds,
    ...otherOpen.filter(os => otherOutstanding[os.id] > 0.0001).map(os => os.id),
  ]));

  const [alloc, setAlloc] = React.useState(() => {
    const m = {};
    candidateIds.forEach(id => {
      const ex = it.so_alloc && it.so_alloc.find(a => a.so_id === id);
      m[id] = ex ? ex.qty : (id === po.so_id && !(it.so_alloc && it.so_alloc.length) ? it.qty : 0);
    });
    return m;
  });
  const [picked, setPicked] = React.useState(() => {
    const m = {}; candidateIds.forEach(id => { m[id] = (alloc[id] || 0) > 0 || (already[id] || 0) > 0; }); return m;
  });

  const floor = (id) => already[id] || 0;
  const total = candidateIds.reduce((s, id) => s + (picked[id] ? (Number(alloc[id]) || 0) : 0), 0);
  const balanced = Math.abs(total - (Number(it.qty) || 0)) < 0.0001;

  const save = () => {
    const entries = candidateIds.filter(id => picked[id] && (Number(alloc[id]) || 0) > 0).map(id => ({ so_id: id, qty: Number(alloc[id]) || 0 }));
    onSave(entries.length >= 2 ? entries : undefined);   // 1 survivor -> back to the plain default
  };

  return (
    <div style={{ padding: '6px 8px', background: 'var(--bg-subtle)', borderRadius: 4, marginTop: 4 }}>
      <div className="tiny muted" style={{ marginBottom: 4 }}>Which SO(s) is this line's {qty(it.qty)} for?</div>
      {candidateIds.map(id => {
        const so = (state.sales_orders || []).find(s => s.id === id);
        const min = floor(id);
        return (
          <div key={id} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '2px 0' }}>
            <input type="checkbox" checked={!!picked[id]} disabled={min > 0}
              onChange={e => setPicked(p => ({ ...p, [id]: e.target.checked }))}/>
            <span className="tiny mono" style={{ width: 130 }}>{so ? so.so_no : id}{id === po.so_id ? ' (original)' : ''}</span>
            {min > 0 && <span className="tiny muted">already received {min} — locked in</span>}
            {picked[id] && (
              <input type="number" min={min} className="input mono" value={alloc[id] || 0}
                onChange={e => setAlloc(a => ({ ...a, [id]: Math.max(min, Number(e.target.value) || 0) }))}
                style={{ width: 64, height: 24, textAlign: 'right', marginLeft: 'auto' }}/>
            )}
          </div>
        );
      })}
      <div style={{ display: 'flex', gap: 8, alignItems: 'center', marginTop: 6 }}>
        <span className="tiny mono" style={{ color: balanced ? 'var(--success)' : 'var(--danger)' }}>{total} / {it.qty}</span>
        <div className="grow"/>
        <button type="button" className="btn btn-sm" onClick={onCancel}>Cancel</button>
        <button type="button" className="btn btn-sm btn-primary" disabled={!balanced} onClick={save}>Save split</button>
      </div>
    </div>
  );
}
window.LinePoSplitEditor = LinePoSplitEditor;

// Per-vendor receive modal (used from the SO Vendor POs tab — scalable to many vendors).
// The PO e-Bill as a document.
//
// "Print e-Bill" used to call window.print(), which prints the BROWSER PAGE —
// sidebar, buttons and all. That is not a document anybody can send, so the
// e-Bill was in practice a record with no output. This renders the real thing
// in its own window.
function poEbillHtml(po, vendor, so, org, getProduct, opts) {
  const autoPrint = !!(opts && opts.autoPrint);
  const e = po.ebill || {};
  const esc = (x) => String(x == null ? '' : x).replace(/[&<>]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));

  const due = po.expected ? fmtDate(po.expected) : '—';
  // Tax per line, from the PO's own configuration. Lines can differ: one may be
  // CGST+SGST, another IGST, a labour charge TDS. The totals below are summed
  // from these, never recomputed at a flat rate.
  let sumBase = 0, sumTax = 0;
  const summaryRows = [];
  const rows = (po.items || []).map((it, n) => {
    const p = getProduct(it.product_id) || {};
    const q = Number(it.qty) || 0;
    const rate = Number(it.rate) || 0;
    const base = q * rate;
    const taxes = poLineTaxes(po, it.product_id);
    const calc = taxesOn(base, taxes);
    sumBase += base; sumTax += calc.tax;
    summaryRows.push({ amount: base, taxes });
    // Each tax on its own line in the cell, so a line carrying GST AND TDS
    // shows both rather than one blended figure nobody can check.
    const taxCell = calc.parts.length
      ? calc.parts.map(x => `<div>${esc(x.label)}</div>`).join('')
      : '<div>—</div>';
    return '<tr>' +
      `<td class="mono">${n + 1}</td>` +
      `<td class="mono">${esc(p.code || it.product_id)}</td>` +
      `<td>${esc(p.name || it.product_id)}</td>` +
      `<td class="mono">${esc(due)}</td>` +
      `<td>${esc(p.uom || 'Nos.')}</td>` +
      `<td class="r mono">${esc(qty(q))}</td>` +
      `<td class="r mono">${esc(inr(rate))}</td>` +
      `<td class="r mono" style="white-space:nowrap">${taxCell}</td>` +
      `<td class="r mono">${esc(inr(calc.total))}</td>` +
      '</tr>';
  }).join('');
  const summary = taxSummary(summaryRows);
  const html = `<!doctype html><html><head><meta charset="utf-8"><title>${esc(e.no || po.po_no)}</title><style>
    *{box-sizing:border-box} body{font-family:-apple-system,Segoe UI,Roboto,Arial,sans-serif;color:#1a1a1a;margin:0;padding:26px;font-size:12.5px}
    .paper{max-width:900px;margin:0 auto;border:1px solid #e2e2e2;border-radius:10px;padding:24px}
    .top{display:flex;justify-content:space-between;align-items:flex-start;border-bottom:2px solid #222;padding-bottom:10px}
    h1{font-size:16px;margin:0}.mut{color:#777}.mono{font-family:ui-monospace,Menlo,Consolas,monospace}
    .title{font-size:14px;font-weight:800;letter-spacing:.07em;text-align:right}
    .grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin:14px 0}
    .lbl{font-size:10px;text-transform:uppercase;letter-spacing:.04em;color:#888}
    table{width:100%;border-collapse:collapse;margin-top:10px;table-layout:fixed}
    th,td{text-align:left;padding:6px 5px;border-bottom:1px solid #eee;font-size:11px;word-break:break-word}
    th{background:#fafafa;font-size:10px;text-transform:uppercase;color:#666}.r{text-align:right}
    tfoot td{border:none;border-top:1px solid #ddd;padding-top:8px;font-size:12px}
    .sign{display:flex;justify-content:space-between;margin-top:36px}.sign div{width:45%;border-top:1px solid #bbb;padding-top:6px;font-size:11px;color:#555}
    .foot{margin-top:16px;font-size:10.5px;color:#999;text-align:center}
    @media print{body{padding:0}.paper{border:none}}
  </style></head><body><div class="paper">
    <div class="top"><div><h1>${esc((org && org.name) || 'Organisation')}</h1>
      <div class="mut">${esc((org && org.address) || '')}</div>
      <div class="mono mut">GSTIN: ${esc((org && org.gstin) || '—')}</div></div>
      <div><div class="title">PURCHASE ORDER e-BILL</div>
        <div class="mono">${esc(e.no || '—')}</div>
        <div class="mut">${esc(fmtDate(e.date || po.date))}</div></div></div>
    <div class="grid">
      <div><div class="lbl">Vendor</div><div><strong>${esc(vendor ? vendor.name : '—')}</strong></div>
        <div class="mut mono">GSTIN: ${esc((vendor && vendor.gstin) || '—')}</div>
        <div class="mut">${esc((vendor && (vendor.address || vendor.city)) || '')}</div></div>
      <div><div class="lbl">Purchase order</div><div class="mono">${esc(po.po_no)}</div>
        <div class="lbl" style="margin-top:8px">Against order</div><div class="mono">${esc(so ? so.so_no : '—')}</div>
        <div class="lbl" style="margin-top:8px">IRN</div><div class="mono" style="font-size:10.5px;word-break:break-all">${esc(e.irn || '—')}</div></div>
    </div>
    <table><thead><tr>
        <th>Sr No.</th><th>Part No.</th><th>Description</th><th>Due on</th><th>Unit</th>
        <th class="r">Qty</th><th class="r">Rate</th><th class="r">Tax</th><th class="r">Amount with tax</th>
      </tr></thead>
      <tbody>${rows}</tbody>
      <tfoot>
        <tr><td colspan="8" class="r">Taxable value</td><td class="r mono">${esc(inr(sumBase))}</td></tr>
        ${summary.map(x => `<tr><td colspan="8" class="r">${esc(x.label)}</td><td class="r mono">${esc(inr(x.amount))}</td></tr>`).join('')}
        <tr><td colspan="8" class="r"><strong>Grand Total</strong></td><td class="r mono"><strong>${esc(inr(sumBase + sumTax))}</strong></td></tr>
      </tfoot></table>
    <div class="sign"><div>For ${esc((org && org.name) || '')}</div><div>Vendor acknowledgement</div></div>
    <div class="foot">Electronically generated purchase order bill — valid without signature.</div>
  </div>${autoPrint ? '<script>window.onload=function(){setTimeout(function(){window.print()},200)}<\/script>' : ''}</body></html>`;
  return html;
}

// Opens the e-Bill in its own window and prints it. Kept for the direct
// "print without looking" path.
function printPOEbill(po, vendor, so, org, getProduct) {
  const w = window.open('', '_blank', 'width=860,height=940');
  if (!w) return false;
  w.document.open();
  w.document.write(poEbillHtml(po, vendor, so, org, getProduct, { autoPrint: true }));
  w.document.close();
  return true;
}

// On-screen preview. The iframe holds the SAME markup that prints, so what is
// on screen is the document — not a second drawing of it that can drift.
function POEbillModal({ po, onClose }) {
  const { state, mutate, getVendor, getSO, getProduct, currentUser, getUser } = useStore();
  const toast = useToast();
  const frame = React.useRef(null);
  const v = getVendor(po.vendor_id);
  const so = getSO(po.so_id);
  const e = po.ebill || {};
  const role = currentUser ? (getUser(currentUser) || {}).role : '';
  const canEditTax = ['Purchase', 'Project Manager', 'Org Admin', 'Managing Director'].includes(role);

  // --- tax editing ---------------------------------------------------------
  // Held locally while it is being worked on, so the document redraws as fast
  // as it is typed, and written to the PO on Save. Nothing is committed by
  // merely opening the dialog.
  const [taxCfg, setTaxCfg] = React.useState(() => JSON.parse(JSON.stringify(po.tax_config || {})));
  const [sel, setSel] = React.useState({});
  const [tab, setTab] = React.useState('doc');      // doc | tax
  const [dirty, setDirty] = React.useState(false);

  const poDraft = { ...po, tax_config: taxCfg };
  const html = poEbillHtml(poDraft, v, so, state.org, getProduct, { autoPrint: false });

  const lineTaxes = (pid) => poLineTaxes(poDraft, pid);

  // Write a whole list onto specific lines.
  const putLines = (pids, taxes) => {
    setTaxCfg(c => {
      const lines = { ...(c.lines || {}) };
      pids.forEach(pid => { lines[pid] = taxes.map(t => ({ key: t.key, rate: Number(t.rate) })); });
      return { ...c, lines };
    });
    setDirty(true);
  };

  // ADD one tax to lines, keeping whatever is already there. A line routinely
  // carries GST and TDS together, so adding must not replace.
  //
  // The same kind twice is refused — two IGST rows on one line is a mistake
  // every time, and would silently double the charge. Adding a kind that is
  // already there updates its rate instead.
  const addTax = (pids, key, rate) => {
    const r = rate == null ? taxKind(key).rate : Number(rate);
    setTaxCfg(c => {
      const lines = { ...(c.lines || {}) };
      pids.forEach(pid => {
        const cur = normaliseTaxes(
          Object.prototype.hasOwnProperty.call(lines, pid) ? lines[pid] : poLineTaxes(poDraft, pid));
        const at = cur.findIndex(t => t.key === key);
        lines[pid] = at === -1 ? [...cur, { key, rate: r }]
                               : cur.map((t, i) => i === at ? { key, rate: r } : t);
      });
      return { ...c, lines };
    });
    setDirty(true);
  };

  const removeTax = (pid, idx) => {
    setTaxCfg(c => {
      const lines = { ...(c.lines || {}) };
      const cur = normaliseTaxes(
        Object.prototype.hasOwnProperty.call(lines, pid) ? lines[pid] : poLineTaxes(poDraft, pid));
      lines[pid] = cur.filter((_, i) => i !== idx);
      return { ...c, lines };
    });
    setDirty(true);
  };

  const setRate = (pid, idx, rate) => {
    setTaxCfg(c => {
      const lines = { ...(c.lines || {}) };
      const cur = normaliseTaxes(
        Object.prototype.hasOwnProperty.call(lines, pid) ? lines[pid] : poLineTaxes(poDraft, pid));
      lines[pid] = cur.map((t, i) => i === idx ? { ...t, rate: Number(rate) || 0 } : t);
      return { ...c, lines };
    });
    setDirty(true);
  };

  const clearTaxes = (pids) => { putLines(pids, []); setSel({}); };

  const selKeys = Object.keys(sel).filter(k => sel[k]);
  const items = po.items || [];
  const allTicked = items.length > 0 && items.every(it => sel[it.product_id]);

  const totals = items.reduce((a, it) => {
    const base = (Number(it.qty) || 0) * (Number(it.rate) || 0);
    const calc = taxesOn(base, lineTaxes(it.product_id));
    return { base: a.base + base, tax: a.tax + calc.tax, total: a.total + calc.total };
  }, { base: 0, tax: 0, total: 0 });
  // Per KIND, the way the document foots it — GST and TDS never blended.
  const summary = taxSummary(items.map(it => ({
    amount: (Number(it.qty) || 0) * (Number(it.rate) || 0),
    taxes: lineTaxes(it.product_id),
  })));

  const saveTax = () => {
    mutate(st => ({
      ...st,
      vendor_pos: st.vendor_pos.map(x => x.id === po.id ? {
        ...x, tax_config: taxCfg,
        // Keep the already-generated e-Bill's own stored amount in step with
        // the tax just saved — this used to only update tax_config, leaving
        // .ebill.amount (read elsewhere, e.g. the PO header's own total)
        // stuck at whatever it was when the e-Bill was first generated, even
        // though the Document tab itself already recomputes live and would
        // disagree with it.
        ...(x.ebill && x.ebill.generated ? { ebill: { ...x.ebill, amount: totals.total } } : {}),
      } : x),
    }), { action: 'po-tax', entity: 'VendorPO', entity_id: po.id,
          detail: `Tax updated on ${po.po_no} · ${inr(totals.total)}` });
    setDirty(false);
    toast(`Tax saved on ${po.po_no} · ${inr(totals.total)}`, 'success');
  };

  const doPrint = () => {
    // Print the frame itself, so the paper is exactly what was previewed.
    // If the browser refuses (some block printing a frame), fall back to the
    // separate window rather than silently doing nothing.
    // While the tax tab is open the frame is not mounted, so print from the
    // draft — which carries any tax edited but not yet saved. What comes out of
    // the printer is what is on screen either way.
    try {
      const w = tab === 'doc' && frame.current && frame.current.contentWindow;
      if (w) { w.focus(); w.print(); return; }
    } catch (err) { /* fall through */ }
    printPOEbill(poDraft, v, so, state.org, getProduct);
  };

  return (
    <Modal title={`PO e-Bill — ${e.no || po.po_no}`} size="xl" onClose={onClose} footer={
      <>
        <span className="tiny muted" style={{ marginRight: 'auto' }}>
          {po.po_no} · {v ? v.name : ''}{so ? ` · ${so.so_no}` : ''}
          {' · '}<strong>{inr(totals.total)}</strong>
          {dirty && <span style={{ color: 'var(--warning)' }}> · unsaved tax changes</span>}
        </span>
        {dirty && <button className="btn" onClick={saveTax}><Icon name="save" size={13}/>Save tax</button>}
        <button className="btn" onClick={onClose}>Close</button>
        <button className="btn btn-primary" onClick={doPrint}><Icon name="print" size={13}/>Print / Download PDF</button>
      </>
    }>
      <div style={{ display: 'flex', gap: 6, marginBottom: 8 }}>
        <button className={`btn btn-sm ${tab === 'doc' ? 'btn-primary' : ''}`} onClick={() => setTab('doc')}>
          <Icon name="receipt" size={12}/>Document
        </button>
        {canEditTax && (
          <button className={`btn btn-sm ${tab === 'tax' ? 'btn-primary' : ''}`} onClick={() => setTab('tax')}>
            <Icon name="edit" size={12}/>Tax ({items.length} line{items.length === 1 ? '' : 's'})
          </button>
        )}
        <span className="tiny muted" style={{ marginLeft: 'auto', alignSelf: 'center' }}>
          {inr(totals.base)} + {totals.tax < 0 ? 'withheld ' : ''}{inr(Math.abs(totals.tax))} = <strong>{inr(totals.total)}</strong>
        </span>
      </div>

      {tab === 'doc' ? (
        <iframe
          ref={frame}
          title="PO e-Bill preview"
          srcDoc={html}
          style={{ width: '100%', height: '58vh', minHeight: 360, border: '1px solid var(--border)',
                   borderRadius: 'var(--radius)', background: '#fff' }}
        />
      ) : (
        <div>
          {/* One tax for the whole PO — what most POs need, in one click. */}
          <div className="card mb-2"><div className="card-body" style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', padding: '10px 12px' }}>
            <span className="tiny muted" style={{ fontWeight: 600 }}>Apply to</span>
            <select className="select" style={{ height: 28, width: 120 }} value={selKeys.length ? 'sel' : 'all'}
              onChange={e2 => { if (e2.target.value === 'all') setSel({}); else setSel(Object.fromEntries(items.map(i => [i.product_id, true]))); }}>
              <option value="all">every line</option>
              <option value="sel">ticked lines{selKeys.length ? ` (${selKeys.length})` : ''}</option>
            </select>
            {TAX_KINDS.filter(k => k.key !== 'none').map(k => (
              <button key={k.key} className="btn btn-sm"
                onClick={() => addTax(selKeys.length ? selKeys : items.map(i => i.product_id), k.key)}
                title={(k.sign < 0 ? 'Withheld from what we pay the vendor' : 'Added to the amount') +
                       ' · adds to whatever these lines already carry'}>
                + {k.label}
              </button>
            ))}
            <button className="btn btn-sm"
              onClick={() => clearTaxes(selKeys.length ? selKeys : items.map(i => i.product_id))}>
              Clear taxes
            </button>
          </div></div>

          <div className="card">
            <div className="card-body flush" style={{ maxHeight: '46vh', overflow: 'auto' }}>
              <table className="t">
                <thead><tr>
                  <th style={{ width: 30 }}>
                    <input type="checkbox" checked={allTicked} title="Tick every line"
                      onChange={e2 => setSel(e2.target.checked ? Object.fromEntries(items.map(i => [i.product_id, true])) : {})}/>
                  </th>
                  <th>Item</th>
                  <th className="num" style={{ width: 56 }}>Qty</th>
                  <th className="num" style={{ width: 96 }}>Amount</th>
                  <th colSpan="2" style={{ width: 300 }}>Taxes <span className="tiny muted">(stack as many as apply)</span></th>
                  <th className="num" style={{ width: 110 }}>With tax</th>
                </tr></thead>
                <tbody>
                  {items.map((it, i) => {
                    const pr = getProduct(it.product_id) || {};
                    const base = (Number(it.qty) || 0) * (Number(it.rate) || 0);
                    const taxes = lineTaxes(it.product_id);
                    const calc = taxesOn(base, taxes);
                    return (
                      <tr key={i}>
                        <td style={{ verticalAlign: 'top', paddingTop: 10 }}>
                          <input type="checkbox" checked={!!sel[it.product_id]}
                            onChange={() => setSel(x => ({ ...x, [it.product_id]: !x[it.product_id] }))}/>
                        </td>
                        <td style={{ verticalAlign: 'top' }}>
                          <div className="small trunc" style={{ maxWidth: 280 }}>{pr.name || it.product_id}</div>
                          <div className="tiny muted mono">{pr.code || ''}</div>
                        </td>
                        <td className="num mono small" style={{ verticalAlign: 'top' }}>{qty(it.qty)}</td>
                        <td className="num mono small" style={{ verticalAlign: 'top' }}>{inr(base)}</td>
                        {/* One row per tax. A line can carry as many as it needs. */}
                        <td colSpan="2">
                          {taxes.length === 0 && <div className="tiny muted">no tax</div>}
                          {taxes.map((t, ti) => {
                            const kind = taxKind(t.key);
                            const part = taxOn(base, t.key, t.rate);
                            return (
                              <div key={ti} style={{ display: 'flex', gap: 4, alignItems: 'center', marginBottom: 3 }}>
                                <select className="select" style={{ height: 24, fontSize: 11, flex: 1, minWidth: 0 }}
                                  value={t.key}
                                  onChange={e2 => {
                                    const next = taxes.map((x, j) => j === ti ? { key: e2.target.value, rate: taxKind(e2.target.value).rate } : x);
                                    putLines([it.product_id], next);
                                  }}>
                                  {TAX_KINDS.filter(k => k.key !== 'none').map(k => (
                                    <option key={k.key} value={k.key}
                                      disabled={k.key !== t.key && taxes.some(x => x.key === k.key)}>{k.label}</option>
                                  ))}
                                </select>
                                <input className="input num" type="number" step="0.1" min="0" value={t.rate}
                                  onChange={e2 => setRate(it.product_id, ti, e2.target.value)}
                                  style={{ height: 24, width: 58, textAlign: 'right', fontSize: 11 }}/>
                                <span className="tiny muted mono" style={{ width: 74, textAlign: 'right' }}
                                  title={kind.split ? `${Math.round((t.rate / 2) * 100) / 100}+${Math.round((t.rate / 2) * 100) / 100}` : ''}>
                                  {inr(part.tax)}
                                </span>
                                <button className="btn btn-ghost btn-sm" title="Remove this tax"
                                  onClick={() => removeTax(it.product_id, ti)}><Icon name="x" size={10}/></button>
                              </div>
                            );
                          })}
                          <select className="select" value="" style={{ height: 22, fontSize: 11, width: 128 }}
                            onChange={e2 => { if (e2.target.value) { addTax([it.product_id], e2.target.value); e2.target.value = ''; } }}>
                            <option value="">+ add tax…</option>
                            {TAX_KINDS.filter(k => k.key !== 'none' && !taxes.some(x => x.key === k.key))
                              .map(k => <option key={k.key} value={k.key}>{k.label}</option>)}
                          </select>
                        </td>
                        <td className="num mono small" style={{ verticalAlign: 'top', color: calc.tax < 0 ? 'var(--danger)' : 'inherit' }}>
                          {inr(calc.total)}
                        </td>
                      </tr>
                    );
                  })}
                </tbody>
                <tfoot>
                  <tr>
                    <td colSpan="3"></td>
                    <td className="num mono small">{inr(totals.base)}</td>
                    <td colSpan="2" className="tiny muted">taxable value</td>
                    <td></td>
                  </tr>
                  {summary.map((x, i) => (
                    <tr key={i}>
                      <td colSpan="4"></td>
                      <td colSpan="2" className="tiny muted">{x.label}</td>
                      <td className="num mono small" style={{ color: x.amount < 0 ? 'var(--danger)' : 'inherit' }}>{inr(x.amount)}</td>
                    </tr>
                  ))}
                  <tr>
                    <td colSpan="4"></td>
                    <td colSpan="2" className="small"><strong>Grand Total</strong></td>
                    <td className="num mono"><strong>{inr(totals.total)}</strong></td>
                  </tr>
                </tfoot>
              </table>
            </div>
          </div>
          <div className="tiny muted mt-2">
            A line can carry <strong>several taxes at once</strong> — GST and TDS together, or GST and TCS.
            Every one is worked out on the <strong>taxable value</strong>, never on a running total, so the
            result does not depend on the order they were added.
            TDS is <strong>withheld</strong> from what we pay the vendor and so reduces the total; GST and TCS add to it.
            The document and the Grand Total follow as you change them.
          </div>
        </div>
      )}
    </Modal>
  );
}

window.poEbillHtml = poEbillHtml;
window.printPOEbill = printPOEbill;
window.POEbillModal = POEbillModal;

function ReceiveModal({ po, onClose }) {
  const { state, mutate, addToPool, getProduct, getVendor, getSO, currentUser, getUser } = useStore();
  const toast = useToast();
  const v = getVendor(po.vendor_id);
  const linkedSoIds = poLinkedSoIds(po);
  // Already received against this PO on an earlier partial delivery.
  const already = {};
  (state.grns || []).forEach(g => {
    if (g.po_id !== po.id) return;
    (g.items || []).forEach(it => { already[it.product_id] = (already[it.product_id] || 0) + (Number(it.accepted) || 0); });
  });
  const outstanding = (it) => Math.max(0, (Number(it.qty) || 0) - (already[it.product_id] || 0));

  // Default to "everything still outstanding arrived", which is the common
  // case. A line already fully received starts unticked rather than inviting a
  // double receipt.
  const [items, setItems] = React.useState(po.items.map(it => {
    const left = outstanding(it);
    return { ...it, recv: left > 0, received: left, rejected: 0, reason: '', to_pool: 0, outstanding: left, so_split: null };
  }));
  const [lr, setLr] = React.useState(po.dispatch_info && po.dispatch_info.lr_no ? po.dispatch_info.lr_no : '');
  const [grnDate, setGrnDate] = React.useState(TODAY);
  const [showExtra, setShowExtra] = React.useState(false);   // rejections / pool
  const isShared = (it) => Array.isArray(it.so_alloc) && it.so_alloc.length > 1;

  const setAll = (on) => setItems(its => its.map(it => on
    ? { ...it, recv: it.outstanding > 0, received: it.outstanding }
    : { ...it, recv: false, received: 0, rejected: 0, to_pool: 0 }));
  const tickedCount = items.filter(it => it.recv).length;
  const receivable = items.filter(it => it.outstanding > 0).length;
  const totalUnits = items.reduce((a, it) => a + (it.recv ? (Number(it.received) || 0) : 0), 0);
  const anyExtra = items.some(it => (Number(it.rejected) || 0) > 0 || (Number(it.to_pool) || 0) > 0);
  const submit = async () => {
    if (items.some(it => it.rejected > 0 && !it.reason)) { toast('Add a reason for each rejected line'); return; }
    for (const it of items) {
      if (!it.recv || !isShared(it)) continue;
      const acc = Math.max(0, (it.received || 0) - (it.rejected || 0) - (it.to_pool || 0));
      const splitTotal = (it.so_split || []).reduce((s, r) => s + (Number(r.qty) || 0), 0);
      if (acc > 0 && Math.abs(splitTotal - acc) > 0.0001) {
        toast(`Split ${getProduct(it.product_id)?.name || it.product_id} across its SOs (currently ${splitTotal} of ${acc})`);
        return;
      }
    }
    const r = await submitPOReceipt(po, items, { lr, grnDate }, { state, mutate, toast, addToPool, getProduct, getVendor, currentUser, getUser });
    toast(r.posted ? `Receipt posted for ${po.po_no}` : `Sent to ${wfReceiving().approverLabel} for acceptance · ${r.units} unit(s)`, 'success');
    onClose();
  };
  return (
    <Modal title={`Receive — ${po.po_no} · ${v ? v.name : ''}${linkedSoIds.length > 1 ? ` · combined · ${linkedSoIds.length} SOs` : ''}`} onClose={onClose} size="lg" footer={<>
      <span className="tiny muted" style={{ marginRight: 'auto' }}>{tickedCount} of {receivable} line(s) · {totalUnits} unit(s)</span>
      <button className="btn" onClick={onClose}>Cancel</button>
      <button className="btn btn-primary" disabled={!tickedCount} onClick={submit}><Icon name="check" size={13}/>Post receipt</button></>}>
      <ComboPOSummary po={po} getSO={getSO} getProduct={getProduct}/>
      <div className="field-row mb-2">
        <div className="field"><label className="field-label">LR / tracking</label><input className="input mono" value={lr} onChange={e => setLr(e.target.value)}/></div>
        <div className="field"><label className="field-label">Received date</label><input type="date" className="input mono" value={grnDate} onChange={e => setGrnDate(e.target.value)}/></div>
      </div>
      <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', marginBottom: 8 }}>
        <button className="btn btn-sm" onClick={() => setAll(true)}>Everything arrived</button>
        <button className="btn btn-sm" onClick={() => setAll(false)}>Clear all</button>
        <span className="tiny muted">then adjust any line that is short</span>
        <label className="tiny muted" style={{ marginLeft: 'auto', display: 'flex', gap: 6, alignItems: 'center', cursor: 'pointer' }}
          title="Only needed when something arrived damaged, or is surplus to this order">
          <input type="checkbox" checked={showExtra || anyExtra} disabled={anyExtra}
            onChange={e => setShowExtra(e.target.checked)}/>
          Something was rejected / is surplus
        </label>
      </div>
      <table className="t">
        <thead><tr>
          <th style={{ width: 34 }}>
            <input type="checkbox" title="Select every line" checked={receivable > 0 && tickedCount === receivable}
              onChange={e => setAll(e.target.checked)}/>
          </th>
          <th>Item</th><th className="num">Ordered</th><th className="num">Still due</th><th className="num">Arrived</th>
          {(showExtra || anyExtra) && <><th className="num">Accepted</th><th className="num">Rejected</th><th className="num">→ Pool</th></>}
        </tr></thead>
        <tbody>
          {items.map((it, i) => {
            const p = getProduct(it.product_id) || { name: it.product_id, code: it.product_id };
            const off = !it.recv; const acc = it.recv ? Math.max(0, (it.received || 0) - (it.rejected || 0) - (it.to_pool || 0)) : 0;
            const colCount = 5 + ((showExtra || anyExtra) ? 3 : 0);
            return (
              <Fragment key={i}>
              <tr style={{ opacity: off ? 0.5 : 1 }}>
                <td><input type="checkbox" checked={!!it.recv} onChange={e => { const on = e.target.checked; const n = [...items]; n[i] = on ? { ...it, recv: true, received: it.qty, rejected: 0, to_pool: 0 } : { ...it, recv: false, received: 0, rejected: 0, to_pool: 0 }; setItems(n); }}/></td>
                <td>{p.name}<div className="tiny muted mono">{p.code}</div></td>
                <td className="num mono small muted">{qty(it.qty)}</td>
                <td className="num mono small">
                  {it.outstanding > 0 ? qty(it.outstanding)
                    : <span className="badge success tiny" title="Fully received on an earlier delivery">done</span>}
                </td>
                <td className="num"><input type="number" min="0" max={it.outstanding} className="input mono" disabled={off} value={it.received} onChange={e => { const n = [...items]; n[i] = { ...it, received: parseInt(e.target.value) || 0 }; setItems(n); }} style={{ width: 66, textAlign: 'right' }}/></td>
                {(showExtra || anyExtra) && <>
                  <td className="num"><input type="number" className="input mono" readOnly value={acc} style={{ width: 56, textAlign: 'right', background: 'var(--bg-subtle)' }}/></td>
                  <td className="num"><input type="number" min="0" className="input mono" disabled={off} value={it.rejected} onChange={e => { const n = [...items]; n[i] = { ...it, rejected: parseInt(e.target.value) || 0 }; setItems(n); }} style={{ width: 56, textAlign: 'right' }}/></td>
                  <td className="num"><input type="number" min="0" className="input mono" disabled={off} value={it.to_pool} onChange={e => { const n = [...items]; n[i] = { ...it, to_pool: parseInt(e.target.value) || 0 }; setItems(n); }} style={{ width: 56, textAlign: 'right' }}/></td>
                </>}
              </tr>
              {it.recv && isShared(it) && acc > 0 && (
                <tr><td colSpan={colCount} style={{ padding: 0 }}>
                  <SoSplitEditor po={po} productId={it.product_id} accepted={acc} value={it.so_split}
                    onChange={v => { const n = [...items]; n[i] = { ...it, so_split: v }; setItems(n); }}/>
                </td></tr>
              )}
              </Fragment>
            );
          })}
        </tbody>
      </table>
      <div className="tiny muted mt-2">
        Posting creates the GRN and puts the stock into this order's Virtual Godown.
        {wfOn('auto_invoice_on_grn') ? ' The vendor payable and the client invoice are raised from it automatically.' : ''}
      </div>
    </Modal>
  );
}
window.ReceiveModal = ReceiveModal;

function GRNNew() {
  const { navigate, state, mutate, getVendor, getProduct, getSO, addToPool, currentUser, getUser } = useStore();
  const toast = useToast();
  // Receivable = POs not yet fully received.
  const blockedPo = (st) => ['Pending MD Approval', 'Rejected', 'On Hold'].includes(st);
  const receivable = state.vendor_pos.filter(p => p.status !== 'Material Received' && !blockedPo(p.status));
  const [poId, setPoId] = React.useState(() => {
    const d = state.vendor_pos.find(p => p.status === 'In Transit') || receivable[0] || state.vendor_pos[0];
    return d ? d.id : '';
  });
  const po = state.vendor_pos.find(p => p.id === poId) || null;
  const [items, setItems] = React.useState([]);
  const [lr, setLr] = React.useState('DELHIVERY-D88234');
  const [grnDate, setGrnDate] = React.useState(TODAY);
  const isShared = (it) => Array.isArray(it.so_alloc) && it.so_alloc.length > 1;
  React.useEffect(() => {
    setItems(po ? po.items.map(it => ({ ...it, recv: true, received: it.qty, accepted: it.qty, rejected: 0, reason: '', to_pool: 0, so_split: null })) : []);
  }, [poId]);

  if (state.vendor_pos.length === 0) return (
    <div className="page">
      <div className="muted tiny mb-1" style={{ cursor: 'pointer' }} onClick={() => navigate('grn')}>
        <Icon name="chevronLeft" size={12}/> GRN
      </div>
      <div className="empty">
        <div className="empty-title">No vendor PO to receive against</div>
        A GRN records material received against a Vendor PO. Create a Vendor PO first.
        <div className="mt-2"><button className="btn" onClick={() => navigate('vendor-pos')}>Go to Vendor POs</button></div>
      </div>
    </div>
  );

  const v = po ? getVendor(po.vendor_id) : null;
  const so = po ? getSO(po.so_id) : null;
  const linkedSoIds = po ? poLinkedSoIds(po) : [];

  const post = async () => {
    if (!po) { toast('Pick a Vendor PO to receive against'); return; }
    if (items.some(it => it.rejected > 0 && !it.reason)) { toast('Add a reason for each rejected line'); return; }
    for (const it of items) {
      if (!it.recv || !isShared(it)) continue;
      const acc = Math.max(0, (it.received || 0) - (it.rejected || 0) - (it.to_pool || 0));
      const splitTotal = (it.so_split || []).reduce((s, r) => s + (Number(r.qty) || 0), 0);
      if (acc > 0 && Math.abs(splitTotal - acc) > 0.0001) {
        toast(`Split ${getProduct(it.product_id)?.name || it.product_id} across its SOs (currently ${splitTotal} of ${acc})`);
        return;
      }
    }
    const r = await submitPOReceipt(po, items, { lr, grnDate }, { state, mutate, toast, addToPool, getProduct, getVendor, currentUser, getUser });
    if (r.posted) {
      const posted = r.result;
      toast(`${posted.grn.grn_no} posted · ${po.po_no} received${posted.surplusUnits ? ` · ${posted.surplusUnits} → Master Pool` : ''}${posted.billCut ? ` · bill −${inrK(posted.billCut)}` : ''}`, 'success');
      navigate(`vendor-pos/${po.id}`);
    } else {
      toast(`Sent to ${wfReceiving().approverLabel} for acceptance · ${r.units} unit(s)`, 'success');
      navigate('grn');
    }
  };

  return (
    <div className="page">
      <div className="page-header">
        <div>
          <div className="muted tiny mb-1" style={{ cursor: 'pointer' }} onClick={() => navigate('grn')}>
            <Icon name="chevronLeft" size={12}/> GRN
          </div>
          <h1 className="page-title">New GRN</h1>
          <div className="page-sub">
            Receiving material against <span className="mono">{po ? po.po_no : '—'}</span>{v ? ` · ${v.name}` : ''}
            {po && (linkedSoIds.length > 1
              ? <> · for <strong>{linkedSoIds.length} Sales Orders</strong>: {linkedSoIds.map(id => getSO(id)?.so_no).filter(Boolean).join(', ')}</>
              : <> · for <span className="mono">{so?.so_no || '—'}</span></>)}
          </div>
        </div>
        <div className="page-actions">
          <button className="btn" onClick={() => navigate('grn')}>Cancel</button>
          <button className="btn btn-primary" onClick={post}>
            <Icon name="check" size={13}/>Post GRN
          </button>
        </div>
      </div>

      <div className="card mb-2"><div className="card-body" style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
        <div className="field" style={{ minWidth: 280 }}>
          <label className="field-label">Receive against Vendor PO</label>
          <select className="select" value={poId} onChange={e => setPoId(e.target.value)}>
            {state.vendor_pos.filter(p => !blockedPo(p.status)).map(p => {
              const vv = getVendor(p.vendor_id); const ss = getSO(p.so_id);
              const n = poLinkedSoIds(p).length;
              return (
                <option key={p.id} value={p.id}>{p.po_no} · {vv?.name} · {ss?.so_no || '—'}{n > 1 ? ` (+${n - 1} more SO${n > 2 ? 's' : ''})` : ''} · {p.status}</option>
              );
            })}
          </select>
        </div>
        <div className="field"><label className="field-label">LR / tracking</label><input className="input mono" value={lr} onChange={e => setLr(e.target.value)}/></div>
        <div className="field"><label className="field-label">Received date</label><input type="date" className="input mono" value={grnDate} onChange={e => setGrnDate(e.target.value)}/></div>
      </div></div>

      {po && <ComboPOSummary po={po} getSO={getSO} getProduct={getProduct}/>}

      <div className="detail-grid">
        <div className="stack">
          <div className="card">
            <div className="card-header"><h3 className="card-title">Receipt Lines</h3></div>
            <div className="card-body flush">
              <table className="t">
                <thead><tr><th style={{ width: 30 }}>Recv</th><th>Item</th><th className="num">Ordered</th><th className="num">Received</th><th className="num">Accepted</th><th className="num">Rejected</th><th className="num">Not needed → Pool</th><th>Reject reason</th></tr></thead>
                <tbody>
                  {items.map((it, i) => {
                    const p = getProduct(it.product_id);
                    const acc = it.recv ? Math.max(0, (it.received || 0) - (it.rejected || 0) - (it.to_pool || 0)) : 0;
                    const off = !it.recv;
                    return (
                      <Fragment key={i}>
                      <tr style={{ opacity: off ? 0.5 : 1 }}>
                        <td><input type="checkbox" checked={!!it.recv} onChange={e => { const on = e.target.checked; const next=[...items]; next[i] = on ? {...it, recv:true, received: it.qty, accepted: it.qty, rejected:0, to_pool:0} : {...it, recv:false, received:0, accepted:0, rejected:0, to_pool:0}; setItems(next); }}/></td>
                        <td>{p.name}<div className="tiny muted mono">{p.code}</div></td>
                        <td className="num">{qty(it.qty)}</td>
                        <td className="num">
                          <input type="number" className="input mono" min="0" value={it.received} disabled={off}
                                 onChange={e => { const v = parseInt(e.target.value) || 0; const next=[...items]; next[i] = {...it, received: v, accepted: Math.max(0, v - it.rejected - (it.to_pool||0))}; setItems(next); }}
                                 style={{ width: 64, textAlign: 'right' }}/>
                        </td>
                        <td className="num">
                          <input type="number" className="input mono" value={acc} readOnly style={{ width: 64, textAlign: 'right', background: 'var(--bg-subtle)' }}/>
                        </td>
                        <td className="num">
                          <input type="number" className="input mono" min="0" value={it.rejected} disabled={off}
                                 onChange={e => { const v = parseInt(e.target.value) || 0; const next=[...items]; next[i] = {...it, rejected: v, accepted: Math.max(0, it.received - v - (it.to_pool||0))}; setItems(next); }}
                                 style={{ width: 64, textAlign: 'right' }}/>
                        </td>
                        <td className="num">
                          <input type="number" className="input mono" min="0" value={it.to_pool || 0} disabled={off}
                                 onChange={e => { const v = parseInt(e.target.value) || 0; const next=[...items]; next[i] = {...it, to_pool: v, accepted: Math.max(0, it.received - it.rejected - v)}; setItems(next); }}
                                 style={{ width: 70, textAlign: 'right' }}/>
                          {(it.to_pool||0) > 0 && <div className="tiny" style={{ color: 'var(--accent)' }}>→ Master Pool</div>}
                        </td>
                        <td><input className="input" placeholder={it.rejected > 0 ? 'Reason required' : ''} disabled={off || !it.rejected} value={it.reason || ''} onChange={e => { const next = [...items]; next[i] = { ...it, reason: e.target.value }; setItems(next); }}/></td>
                      </tr>
                      {it.recv && isShared(it) && acc > 0 && po && (
                        <tr><td colSpan={7} style={{ padding: 0 }}>
                          <SoSplitEditor po={po} productId={it.product_id} accepted={acc} value={it.so_split}
                            onChange={v => { const next = [...items]; next[i] = { ...it, so_split: v }; setItems(next); }}/>
                        </td></tr>
                      )}
                      </Fragment>
                    );
                  })}
                </tbody>
              </table>
            </div>
          </div>
          <div className="card">
            <div className="card-header"><h3 className="card-title">QC Checklist</h3></div>
            <div className="card-body">
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 6 }}>
                {['Outer packaging intact','Quantity matches PO','Serial numbers recorded','Visual inspection passed','Documentation attached','No damage observed'].map((c, i) => (
                  <label key={i} className="small" style={{ display: 'flex', alignItems: 'center', gap: 8, padding: 6, border: '1px solid var(--border)', borderRadius: 4, cursor: 'pointer' }}>
                    <input type="checkbox" defaultChecked/>
                    {c}
                  </label>
                ))}
              </div>
              <div className="field mt-2">
                <label className="field-label">Stores remarks</label>
                <textarea className="textarea" placeholder="Any observations…"/>
              </div>
            </div>
          </div>
        </div>
        <div className="stack">
          <div className="card">
            <div className="card-header"><h3 className="card-title">Receipt Info</h3></div>
            <div className="card-body">
              <div className="dl">
                <dt>Vendor PO</dt><dd className="mono">{po ? po.po_no : '—'}</dd>
                <dt>For SO</dt><dd className="mono">{so?.so_no || '—'}</dd>
                <dt>LR / tracking</dt><dd className="mono">{lr}</dd>
                <dt>Received</dt><dd className="mono">{fmtDate(grnDate)}</dd>
              </div>
              <div className="field mt-2"><label className="field-label">Vehicle no.</label><input className="input mono" defaultValue="MH-04-EZ-9921"/></div>
            </div>
          </div>
          <div className="card">
            <div className="card-header"><h3 className="card-title">Photos</h3></div>
            <div className="card-body">
              <div className="ph-block">Drop photos here · up to 10</div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// ===== 3-Way Match (real routing: GRN → record vendor invoice → match → book) =====
function ThreeWayMatchList() {
  const { state, navigate, currentUser, getUser, getVendor } = useStore();
  const [showRecord, setShowRecord] = React.useState(false);
  const role = currentUser ? getUser(currentUser)?.role : '';
  const canRecord = ['Billing', 'Purchase', 'Org Admin'].includes(role);
  const stBadge = (st) => st === 'Booked' ? <span className="badge success dot">Booked</span>
    : st === 'Rejected' ? <span className="badge danger dot">Rejected</span>
    : st === 'Parked' ? <span className="badge warning dot">Parked</span>
    : <span className="badge accent dot">{st}</span>;

  return (
    <div className="page">
      <div className="page-header">
        <div>
          <h1 className="page-title">3-Way Match</h1>
          <div className="page-sub">Vendor Invoice ⟷ Vendor PO ⟷ GRN · auto tolerance check before booking payables</div>
        </div>
        <div className="page-actions">
          {canRecord && <button className="btn btn-primary" onClick={() => setShowRecord(true)}><Icon name="plus" size={13}/>Record vendor invoice</button>}
        </div>
      </div>

      <div className="card">
        <div className="card-body flush">
          <table className="t">
            <thead><tr>
              <th>Vendor Invoice</th><th>Vendor</th><th>PO</th><th>GRN</th>
              <th className="num">Amount</th><th>Tolerance</th><th>Status</th><th></th>
            </tr></thead>
            <tbody>
              {state.vendor_invoices.map(vi => {
                const v = getVendor(vi.vendor_id);
                const po = state.vendor_pos.find(p => p.id === vi.po_id);
                return (
                  <tr key={vi.id} onClick={() => navigate(`three-way/${vi.id}`)} style={{ cursor: 'pointer' }}>
                    <td className="mono">{vi.vendor_invoice_no}</td>
                    <td>{v?.name}</td>
                    <td className="mono">{po?.po_no}</td>
                    <td>{vi.grn_id ? <span className="badge success dot">Received</span> : <span className="badge warning dot">Pending</span>}</td>
                    <td className="num">{inr(vi.amount)}</td>
                    <td>{vi.tolerance === 'within' ? <span className="badge success">Within</span> : <span className="badge danger">Outside</span>}</td>
                    <td>{stBadge(vi.status)}</td>
                    <td><Icon name="chevronRight" size={12}/></td>
                  </tr>
                );
              })}
              {state.vendor_invoices.length === 0 && (
                <tr><td colSpan="8"><div className="empty">
                  <div className="empty-title">No vendor invoices yet</div>
                  Post a GRN against a Vendor PO, then record the vendor's invoice here — it's auto-checked against the PO &amp; GRN, then booked.
                  {canRecord && <div className="mt-2"><button className="btn btn-primary" onClick={() => setShowRecord(true)}><Icon name="plus" size={13}/>Record vendor invoice</button></div>}
                </div></td></tr>
              )}
            </tbody>
          </table>
        </div>
      </div>
      {showRecord && <RecordVendorInvoiceModal onClose={() => setShowRecord(false)}/>}
    </div>
  );
}

// Record a vendor's invoice against a received PO; tolerance auto-computed.
function RecordVendorInvoiceModal({ onClose, poId }) {
  const { state, mutate, getVendor, getSO } = useStore();
  const toast = useToast();
  const tol = state.config.three_way_value_tolerance != null ? state.config.three_way_value_tolerance : 2;
  // Eligible: POs with a posted GRN and no invoice recorded yet.
  const eligible = state.vendor_pos.filter(p => state.grns.find(g => g.po_id === p.id) && !state.vendor_invoices.find(vi => vi.po_id === p.id));
  const [pid, setPid] = React.useState(poId || (eligible[0] ? eligible[0].id : ''));
  const po = state.vendor_pos.find(p => p.id === pid) || null;
  const grn = po ? state.grns.find(g => g.po_id === po.id) : null;
  const [invNo, setInvNo] = React.useState('');
  const [amount, setAmount] = React.useState(po ? po.amount : 0);
  const [date, setDate] = React.useState(TODAY);
  React.useEffect(() => { setAmount(po ? po.amount : 0); }, [pid]);

  const valueVar = po && po.amount ? ((Number(amount) - po.amount) / po.amount) * 100 : 0;
  const within = Math.abs(valueVar) <= tol;

  const submit = () => {
    if (!po) { toast('Pick a received Vendor PO'); return; }
    // Invoice number is optional — auto-number if left blank.
    const finalNo = invNo.trim() || vendorInvoiceNo(po ? po.po_no : '');
    // Auto-book clean matches; only out-of-tolerance invoices stop for review.
    const vendorName = getVendor(po.vendor_id)?.name || 'vendor';
    const vi = {
      id: 'vi-' + Date.now(), vendor_invoice_no: finalNo, po_id: po.id, grn_id: grn ? grn.id : null,
      vendor_id: po.vendor_id, date, amount: Number(amount) || 0,
      status: within ? 'Booked' : 'Pending 3-Way Match',
      tolerance: within ? 'within' : 'outside',
    };
    mutate(s => ({
      ...s,
      vendor_invoices: [vi, ...s.vendor_invoices],
      notifications: [{
        id: 'n-vi-' + Date.now(), kind: 'match',
        text: within
          ? `${vi.vendor_invoice_no} auto-matched & booked · ${inrK(vi.amount)} payable to ${vendorName} (${po.po_no})`
          : `${vi.vendor_invoice_no} OUTSIDE tolerance · needs review · ${po.po_no}`,
        date: TODAY, read: false, role: within ? 'Managing Director' : 'Billing',
      }, ...s.notifications],
    }), { action: 'create', entity: 'VendorInvoice', entity_id: vi.id });
    toast(within ? `${vi.vendor_invoice_no} matched & booked automatically` : `${vi.vendor_invoice_no} flagged — needs review`, within ? 'success' : '');
    onClose();
  };

  return (
    <Modal title="Record vendor invoice" onClose={onClose} footer={
      <>
        <button className="btn" onClick={onClose}>Cancel</button>
        <button className="btn btn-primary" disabled={!po} onClick={submit}>Record &amp; run match</button>
      </>
    }>
      {eligible.length === 0 && !poId ? (
        <div className="empty">No received POs awaiting an invoice. Post a GRN against a Vendor PO first.</div>
      ) : (
        <>
          <div className="field">
            <label className="field-label">Vendor PO (received) *</label>
            <select className="select" value={pid} onChange={e => setPid(e.target.value)} disabled={!!poId}>
              {(poId ? state.vendor_pos.filter(p => p.id === poId) : eligible).map(p => {
                const v = getVendor(p.vendor_id); const so = getSO(p.so_id);
                return <option key={p.id} value={p.id}>{p.po_no} · {v?.name} · {so?.so_no || '—'} · {inr(p.amount)}</option>;
              })}
            </select>
          </div>
          <div className="field-row mt-2">
            <div className="field"><label className="field-label">Vendor invoice no.</label><input className="input mono" placeholder="optional — auto-numbered if blank" value={invNo} onChange={e => setInvNo(e.target.value)}/></div>
            <div className="field"><label className="field-label">Invoice date</label><input type="date" className="input mono" value={date} onChange={e => setDate(e.target.value)}/></div>
          </div>
          <div className="field mt-2">
            <label className="field-label">Invoice amount</label>
            <input type="number" min="0" className="input mono" value={amount} onChange={e => setAmount(e.target.value)} style={{ width: 180 }}/>
          </div>
          {po && (
            <div className="mt-2" style={{ padding: 10, borderRadius: 4, background: within ? 'var(--success-bg)' : 'var(--danger-bg)', fontSize: 12.5 }}>
              PO amount {inr(po.amount)} · invoice {inr(Number(amount) || 0)} · variance <strong>{valueVar >= 0 ? '+' : ''}{valueVar.toFixed(1)}%</strong> — {within ? `within ±${tol}% tolerance` : `OUTSIDE ±${tol}% tolerance (will flag for review)`}
            </div>
          )}
        </>
      )}
    </Modal>
  );
}

function ThreeWayMatchDetail({ viId }) {
  const { state, navigate, mutate, getVendor, getProduct } = useStore();
  const toast = useToast();
  const vi = state.vendor_invoices.find(v => v.id === viId);
  if (!vi) return <div className="page"><div className="empty">{state.loaded ? 'Not found' : 'Loading…'}</div></div>;
  const po = state.vendor_pos.find(p => p.id === vi.po_id);
  const grn = state.grns.find(g => g.id === vi.grn_id);
  const v = getVendor(vi.vendor_id);
  const tol = state.config.three_way_value_tolerance != null ? state.config.three_way_value_tolerance : 2;
  const valueVar = po && po.amount ? ((vi.amount - po.amount) / po.amount) * 100 : 0;
  const within = Math.abs(valueVar) <= tol;
  const done = ['Booked', 'Rejected'].includes(vi.status);

  const setStatus = (status, msg, kind, notify) => {
    mutate(s => ({
      ...s,
      vendor_invoices: s.vendor_invoices.map(x => x.id === vi.id ? { ...x, status } : x),
      ...(notify ? { notifications: [{ id: 'n-3w-' + Date.now(), kind: 'po', text: notify, date: TODAY, read: false, role: 'Managing Director' }, ...s.notifications] } : {}),
    }), { action: status.toLowerCase(), entity: 'VendorInvoice', entity_id: vi.id });
    toast(msg, kind || '');
    navigate('three-way');
  };

  return (
    <div className="page">
      <div className="page-header">
        <div>
          <div className="muted tiny mb-1" style={{ cursor: 'pointer' }} onClick={() => navigate('three-way')}>
            <Icon name="chevronLeft" size={12}/> 3-Way Match
          </div>
          <h1 className="page-title">3-Way Match — <span className="mono">{vi.vendor_invoice_no}</span>
            <span style={{ marginLeft: 8 }}><span className={`badge dot ${vi.status === 'Booked' ? 'success' : vi.status === 'Rejected' ? 'danger' : 'accent'}`}>{vi.status}</span></span>
          </h1>
          <div className="page-sub">{v?.name} · invoice amount {inr(vi.amount)} · PO {po?.po_no} · GRN {grn?.grn_no || 'pending'}</div>
        </div>
        <div className="page-actions">
          {!done && <button className="btn btn-danger" onClick={() => setStatus('Rejected', 'Invoice rejected · vendor notified', '', `${vi.vendor_invoice_no} REJECTED at 3-way match`)}>Reject</button>}
          {!done && <button className="btn" onClick={() => setStatus('Parked', 'Parked for PM review')}>Park for review</button>}
          {!done && <button className="btn btn-primary" onClick={() => setStatus('Booked', 'Booked in payables · payment scheduled', 'success', `${vi.vendor_invoice_no} booked · ${inrK(vi.amount)} payable to ${v?.name}`)}><Icon name="check" size={13}/>Approve &amp; Book</button>}
          {done && <span className="badge dot">{vi.status}</span>}
        </div>
      </div>

      <div className="mb-2" style={{ padding: 10, background: within ? 'var(--success-bg)' : 'var(--danger-bg)', border: '1px solid', borderColor: within ? 'oklch(0.85 0.06 155)' : 'oklch(0.86 0.08 25)', borderRadius: 4, display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5 }}>
        <Icon name={within ? 'check' : 'alert'} size={14} color={within ? 'var(--success)' : 'var(--danger)'}/>
        <span><strong>{within ? 'Within tolerance' : 'Outside tolerance'}</strong> · value variance {valueVar >= 0 ? '+' : ''}{valueVar.toFixed(1)}% (±{tol}% allowed){!within && ' · PM review recommended before booking'}</span>
      </div>

      <div className="compare">
        <div className="compare-h">Field</div>
        <div className="compare-h">Vendor PO · {po?.po_no}</div>
        <div className="compare-h">GRN · {grn?.grn_no || 'Pending'}</div>
        <div className="compare-h">Vendor Invoice · {vi.vendor_invoice_no}</div>

        <div className="compare-row-label">Vendor</div>
        <div>{v?.name}</div><div>{v?.name}</div><div className="ok">{v?.name} <Icon name="check" size={11}/></div>

        <div className="compare-row-label">Date</div>
        <div className="mono">{fmtDate(po?.date)}</div><div className="mono">{grn ? fmtDate(grn.date) : '—'}</div><div className="mono">{fmtDate(vi.date)}</div>

        {(po?.items || []).map((it, i) => {
          const p = getProduct(it.product_id);
          const grnLine = grn?.items.find(x => x.product_id === it.product_id);
          const qtyMatch = !grnLine || grnLine.accepted === it.qty;
          return (
            <Fragment key={i}>
              <div className="compare-row-label">{p ? p.name : it.product_id}</div>
              <div className="num">Qty {it.qty} @ {inr(it.rate)}</div>
              <div className={`num ${qtyMatch ? 'ok' : 'warn'}`}>{grnLine ? `Acc ${grnLine.accepted} / Rej ${grnLine.rejected || 0}` : '—'} {!qtyMatch && <Icon name="alert" size={11}/>}</div>
              <div className="num">{inr(it.qty * it.rate)}</div>
            </Fragment>
          );
        })}

        <div className="compare-row-label">Subtotal</div>
        <div className="num">{inr(po?.amount || 0)}</div>
        <div className="num">—</div>
        <div className={`num ${within ? 'ok' : 'warn'}`}>{inr(vi.amount)}</div>

        <div className="compare-row-label">Variance</div>
        <div>—</div>
        <div>—</div>
        <div className={within ? 'ok' : 'warn'}>{valueVar >= 0 ? '+' : ''}{valueVar.toFixed(1)}% ({within ? `within ±${tol}%` : `outside ±${tol}%`})</div>
      </div>

      <div className="mt-3 split-2">
        <div className="card">
          <div className="card-header"><h3 className="card-title">TDS &amp; RCM</h3></div>
          <div className="card-body">
            <div className="dl">
              <dt>TDS rate</dt><dd>2% (194Q)</dd>
              <dt>TDS amount</dt><dd className="mono">{inr(vi.amount * 0.02)}</dd>
              <dt>Net payable</dt><dd className="mono"><strong>{inr(vi.amount - vi.amount * 0.02)}</strong></dd>
              <dt>RCM applicable</dt><dd>No (vendor is registered)</dd>
            </div>
          </div>
        </div>
        <div className="card">
          <div className="card-header"><h3 className="card-title">Payment Schedule (vendor / payable)</h3></div>
          <div className="card-body">
            <div className="dl">
              <dt>Vendor terms</dt><dd>{v?.terms}</dd>
              <dt>Invoice date</dt><dd className="mono">{fmtDate(vi.date)}</dd>
              <dt>Due date</dt><dd className="mono">{fmtDate(new Date(new Date(vi.date + 'T00:00:00').getTime() + 30 * 86400000).toISOString().slice(0, 10))}</dd>
              <dt>Status</dt><dd>{vi.status === 'Booked' ? <span className="badge success">Scheduled</span> : <span className="badge">{vi.status}</span>}</dd>
            </div>
            <div className="tiny muted mt-2">This is the <strong>vendor payable</strong> — separate from the customer invoice/collections on the Sales Order.</div>
          </div>
        </div>
      </div>
    </div>
  );
}

window.RecordVendorInvoiceModal = RecordVendorInvoiceModal;

// ===== Sourcing → Vendor PO bridge =====
// In the new flow the vendor is already chosen during the inquiry, so there is
// NO RFQ: once the SO is approved, Purchase generates the Vendor PO(s) straight
// from the inquiry's selected vendors at the sourced prices.

function soSourcing(state, soId) {
  return (state.sourcings || []).find(s => s.converted_so_id === soId) || null;
}

// True required component qty for an SO (bundle_qty × component qty), per product.
// Implementation BOQ requirement (catalogue items only — custom/service items
// have no product_id and are not procured through vendor POs). product_id → qty.
function soImplReq(so) {
  const m = {};
  const boq = (so.extra && so.extra.implementation && so.extra.implementation.boq) || [];
  boq.forEach(b => { if (b.product_id) m[b.product_id] = (m[b.product_id] || 0) + (Number(b.qty) || 0); });
  return m;
}
window.soImplReq = soImplReq;

function soReqComponents(so) {
  const m = soRequired(so);
  // Implementation BOQ (catalogue items) is procured alongside supply on the same SO.
  const impl = soImplReq(so);
  Object.keys(impl).forEach(pid => { m[pid] = (m[pid] || 0) + impl[pid]; });
  // Subtract anything fulfilled from the Master Surplus Pool so we never re-buy it.
  (so.pool_alloc || []).forEach(a => { if (m[a.product_id] != null) m[a.product_id] = m[a.product_id] - (Number(a.qty) || 0); });
  Object.keys(m).forEach(k => { if (m[k] <= 0) delete m[k]; });
  return m;
}
window.soReqComponents = soReqComponents;

// What vendorPOGroups should actually raise a PO for RIGHT NOW, as opposed to
// what the order originally needed in total (soReqComponents, unchanged --
// still used everywhere that reports the order's total requirement).
//
// Nets the original requirement against whatever is already on a vendor PO
// for this SO, then adds soRejectedOutstanding — the replacement quantity a
// client rejection still owes, on an organization running client_acceptance.
// The two additions never overlap: soRejectedOutstanding only counts a vendor
// PO placed BEYOND the original requirement (a genuine replacement), while
// this function's own netting only closes the gap UP TO that original
// requirement -- so a product can never be double-subtracted or double-owed.
//
// On the very first call for an SO (no vendor PO exists yet), onPO is empty
// for every product, so this returns exactly soReqComponents(so) -- byte for
// byte the same as before this existed. Every existing caller of
// generateVendorPOsFromSourcing only ever calls it once, so nothing about
// today's behaviour changes; this only matters the second time it runs,
// which is what lets Purchase float RFQ / raise a Vendor PO again for a
// rejected item through the same screen, instead of a dead end. See
// docs/client-acceptance.md.
function soOutstandingProcurement(state, so) {
  const req = soReqComponents(so);
  const onPO = {};
  (state.vendor_pos || []).forEach(po => {
    if (!poServesSO(po, so.id) || ['Rejected', 'Cancelled'].includes(po.status)) return;
    (po.items || []).forEach(it => { onPO[it.product_id] = (onPO[it.product_id] || 0) + poLineSoQty(po, it.product_id, so.id); });
  });
  const rejected = (typeof soRejectedOutstanding === 'function') ? soRejectedOutstanding(state, so) : {};
  const out = {};
  new Set([...Object.keys(req), ...Object.keys(rejected)]).forEach(pid => {
    const gap = Math.max(0, (req[pid] || 0) - (onPO[pid] || 0));
    const owed = gap + (rejected[pid] || 0);
    if (owed > 0.0001) out[pid] = owed;
  });
  return out;
}
window.soOutstandingProcurement = soOutstandingProcurement;

// Group an SO's required components by the vendor chosen during sourcing, each at
// the sourced unit price. Components without a saved pick fall back to the
// cheapest vendor / baseline so nothing is left unsourced.
function vendorPOGroups(state, so, sourcing, getProduct) {
  const req = soOutstandingProcurement(state, so);
  const alloc = (sourcing && sourcing.alloc) || {};
  const picks = (sourcing && sourcing.picks) || {};
  const prices = (sourcing && sourcing.prices) || {};
  const rateFor = (vid, p, pid) => (prices[pid] && prices[pid][vid] != null) ? prices[pid][vid] : (window.vendorUnitPrice ? window.vendorUnitPrice(vid, p) : (p ? p.buy : 0));
  const groups = {};
  Object.entries(req).forEach(([pid, qty]) => {
    const p = getProduct(pid);
    const rows = (alloc[pid] || []).filter(r => (Number(r.qty) || 0) > 0);
    if (rows.length) {                       // multi-vendor split allocation (clamped to required)
      let remaining = qty;
      rows.forEach(r => {
        const take = Math.min(Number(r.qty) || 0, remaining); if (take <= 0) return; remaining -= take;
        (groups[r.vendor_id] = groups[r.vendor_id] || []).push({ product_id: pid, qty: take, rate: (Number(r.rate) || rateFor(r.vendor_id, p, pid)) });
      });
    } else {                                 // single chosen vendor (or cheapest)
      let vid = picks[pid];
      if (!vid && p && window.vendorSuggestions) { const sug = window.vendorSuggestions(p, state.vendors); if (sug[0]) vid = sug[0].vendor.id; }
      if (!vid) return;
      (groups[vid] = groups[vid] || []).push({ product_id: pid, qty, rate: rateFor(vid, p, pid) });
    }
  });
  return Object.entries(groups).map(([vendor_id, items]) => ({
    vendor_id, items, amount: items.reduce((s, i) => s + i.qty * i.rate, 0),
  }));
}

// Create one Vendor PO per selected vendor and move the SO into procurement.
function generateVendorPOsFromSourcing(so, sourcing, ctx) {
  const { state, mutate, toast, navigate, getProduct } = ctx;
  const groups = vendorPOGroups(state, so, sourcing, getProduct);
  if (groups.length === 0) { toast('No vendor selections found to generate POs'); return; }
  // Issued one at a time, each aware of the ones already handed out in this
  // same action, so a batch of POs cannot collide.
  const __issued = state.vendor_pos.map(p => p.po_no);
  const nextPo = () => { const n = vendorPoNo({ vendor_pos: __issued.map(x => ({ po_no: x })) }, TODAY); __issued.push(n); return n; };
  const expected = (() => { const d = new Date(TODAY); d.setDate(d.getDate() + 7); return d.toISOString().slice(0, 10); })();
  const pos = groups.map((g, i) => ({
    id: 'po-' + Date.now() + '-' + i,
    po_no: nextPo(),
    so_id: so.id, vendor_id: g.vendor_id, date: TODAY, expected,
    status: g.amount > (state.config.vendor_po_md_threshold ?? 500000) ? 'Pending MD Approval' : 'Issued',
    amount: g.amount, items: g.items, ebill: {}, source: 'sourcing',
  }));
  const anyMD = pos.some(p => p.status === 'Pending MD Approval');
  mutate(s => ({
    ...s,
    vendor_pos: [...pos, ...s.vendor_pos],
    sales_orders: s.sales_orders.map(x => x.id === so.id ? { ...x, status: soAdvanceStatus(x.status, 'Procurement Started') } : x),
    notifications: [
      { id: 'n-genpo-' + Date.now(), kind: 'po', text: `${pos.length} Vendor PO(s) raised for ${so.so_no} from selected vendors${anyMD ? ' · some need MD approval' : ''}`, date: TODAY, read: false, role: anyMD ? 'Managing Director' : 'Stores' },
      ...s.notifications,
    ],
  }), { action: 'generate-po', entity: 'SalesOrder', entity_id: so.id });
  toast(`${pos.length} Vendor PO(s) created from inquiry`, 'success');
  navigate('vendor-pos');
}

window.soSourcing = soSourcing;
window.vendorPOGroups = vendorPOGroups;
window.generateVendorPOsFromSourcing = generateVendorPOsFromSourcing;

// ===== Shared: aggregate an SO's required components =====
function procComponentList(so) {
  return soRequiredList(so);
}

// Turn CreateVendorPOModal's rows (each row's own qty + an optional combine[]
// of {so_id, qty} picked up from OTHER SOs sharing the same need) into real PO
// line items. so_alloc is written only when a line genuinely ends up with a
// second contributing SO — a plain, uncombined row keeps today's exact
// single-SO shape (no so_alloc key at all).
function buildComboPOItems(primarySoId, items) {
  const totalOf = (i) => (Number(i.qty) || 0) + (i.combine || []).reduce((s, c) => s + (Number(c.qty) || 0), 0);
  return (items || []).filter(i => totalOf(i) > 0).map(i => {
    const combine = (i.combine || []).filter(c => (Number(c.qty) || 0) > 0);
    const base = { product_id: i.product_id, qty: totalOf(i), rate: i.rate };
    return combine.length
      ? { ...base, so_alloc: [{ so_id: primarySoId, qty: Number(i.qty) || 0 }, ...combine.map(c => ({ so_id: c.so_id, qty: Number(c.qty) || 0 }))] }
      : base;
  });
}
window.buildComboPOItems = buildComboPOItems;

// Shared commit path for every screen that can create a combo PO —
// CreateVendorPOModal (one SO, per-item "combine?") and BulkVendorPOModal
// (several SOs ticked up front) both funnel through here. One definition
// for "what actually happens when a combo PO is created": builds the real
// items, advances every linked SO to Procurement Started, notifies by name.
// Returns null when there is nothing to create (so each caller's own toast
// stays accurate) rather than silently doing nothing.
function createComboVendorPO({ state, mutate, getVendor, getSO, primarySoId, vendorId, items, expected }) {
  const real = buildComboPOItems(primarySoId, items);
  if (!primarySoId || !vendorId || real.length === 0) return null;
  const amount = real.reduce((s, it) => s + (Number(it.qty) || 0) * (Number(it.rate) || 0), 0);
  const needsMD = amount > (state.config.vendor_po_md_threshold ?? 500000);
  const poNo = vendorPoNo(state, TODAY);
  const linkedSoIds = Array.from(new Set([primarySoId, ...real.flatMap(i => (i.so_alloc || []).map(a => a.so_id))]));
  const po = { id: 'po-' + Date.now(), po_no: poNo, so_id: primarySoId, vendor_id: vendorId, date: TODAY, expected, status: needsMD ? 'Pending MD Approval' : 'Issued', amount, items: real, ebill: {}, source: 'manual' };
  mutate(s => ({
    ...s,
    vendor_pos: [po, ...s.vendor_pos],
    sales_orders: s.sales_orders.map(x => linkedSoIds.includes(x.id) ? { ...x, status: soAdvanceStatus(x.status, 'Procurement Started') } : x),
    notifications: [{ id: 'n-po-' + Date.now(), kind: 'po', text: `${poNo} ${needsMD ? 'awaiting MD approval' : 'issued'} → ${getVendor(vendorId)?.name} for ${linkedSoIds.map(id => getSO(id)?.so_no).filter(Boolean).join(', ')} · ${inrK(amount)}`, date: TODAY, read: false, role: needsMD ? 'Managing Director' : 'Stores' }, ...s.notifications],
  }), { action: 'create', entity: 'VendorPO', entity_id: po.id });
  return { po, linkedSoIds, needsMD, amount };
}
window.createComboVendorPO = createComboVendorPO;

// ===== Create Vendor PO for an SO (Purchase) =====
function CreateVendorPOModal({ soId, vendorId, onClose }) {
  const { state, mutate, getSO, getProduct, getVendor } = useStore();
  const toast = useToast();
  const [so, setSo] = React.useState(soId || '');
  const [vendor, setVendor] = React.useState(vendorId || '');
  const soObj = so ? getSO(so) : null;
  const [items, setItems] = React.useState([]);
  const [expected, setExpected] = React.useState(() => { const d = new Date(TODAY); d.setDate(d.getDate() + 7); return d.toISOString().slice(0, 10); });

  // Default to the vendor chosen during the inquiry (most-picked), if any.
  React.useEffect(() => {
    if (vendorId || !soObj) return;
    const sourcing = window.soSourcing ? window.soSourcing(state, so) : null;
    if (sourcing && sourcing.picks) {
      const counts = {}; Object.values(sourcing.picks).forEach(vid => { counts[vid] = (counts[vid] || 0) + 1; });
      const top = Object.entries(counts).sort((a, b) => b[1] - a[1])[0];
      if (top) setVendor(top[0]);
    }
  }, [so]);

  // Load items with sourced prices for the chosen vendor (falls back to baseline).
  React.useEffect(() => {
    if (!soObj) { setItems([]); return; }
    const sourcing = window.soSourcing ? window.soSourcing(state, so) : null;
    const prices = (sourcing && sourcing.prices) || {};
    setItems(procComponentList(soObj).map(c => {
      const p = getProduct(c.product_id);
      let rate = p ? p.buy : 0;
      if (vendor && prices[c.product_id] && prices[c.product_id][vendor] != null) rate = prices[c.product_id][vendor];
      else if (vendor && window.vendorUnitPrice && p) rate = window.vendorUnitPrice(vendor, p);
      return { product_id: c.product_id, qty: c.qty, rate, combine: [] };
    }));
  }, [so, vendor]);

  // Every OTHER open SO that independently needs some of the same item —
  // netted against what each already has on its own vendor PO
  // (soOutstandingProcurement), so an SO that is already fully covered never
  // shows up as a candidate to combine. Purely a deliberate per-PO human
  // click: nothing here writes anything until Create PO is actually pressed.
  const [combineOpen, setCombineOpen] = React.useState({});
  const otherOpenSOs = state.sales_orders.filter(s => s.id !== so && !['Closed', 'Cancelled'].includes(s.status));
  const otherOutstanding = {};
  otherOpenSOs.forEach(os => { otherOutstanding[os.id] = window.soOutstandingProcurement ? window.soOutstandingProcurement(state, os) : {}; });
  const candidatesFor = (pid) => otherOpenSOs
    .map(os => ({ so_id: os.id, so_no: os.so_no, outstanding: otherOutstanding[os.id][pid] || 0 }))
    .filter(c => c.outstanding > 0.0001);

  const setItem = (i, patch) => setItems(its => its.map((x, j) => j === i ? { ...x, ...patch } : x));
  const rowTotalQty = (it) => (Number(it.qty) || 0) + (it.combine || []).reduce((s, c) => s + (Number(c.qty) || 0), 0);
  const toggleCombine = (i, soId, outstanding) => setItems(its => its.map((x, j) => {
    if (j !== i) return x;
    const has = (x.combine || []).some(c => c.so_id === soId);
    return { ...x, combine: has ? x.combine.filter(c => c.so_id !== soId) : [...(x.combine || []), { so_id: soId, qty: outstanding }] };
  }));
  const setCombineQty = (i, soId, qty) => setItems(its => its.map((x, j) => j === i
    ? { ...x, combine: (x.combine || []).map(c => c.so_id === soId ? { ...c, qty: Math.max(0, Number(qty) || 0) } : c) } : x));

  const amount = items.reduce((s, i) => s + rowTotalQty(i) * (i.rate || 0), 0);
  const needsMD = amount > (state.config.vendor_po_md_threshold ?? 500000);

  const submit = () => {
    if (!so || !vendor) { toast('Pick SO, vendor and at least one item'); return; }
    const result = createComboVendorPO({ state, mutate, getVendor, getSO, primarySoId: so, vendorId: vendor, items, expected });
    if (!result) { toast('Pick SO, vendor and at least one item'); return; }
    toast(`${result.po.po_no} created${result.linkedSoIds.length > 1 ? ` · combined across ${result.linkedSoIds.length} SOs` : ''}${result.needsMD ? ' · sent to MD' : ''}`, 'success');
    onClose();
  };

  return (
    <Modal title="Create Vendor PO" onClose={onClose} size="lg" footer={
      <>
        <button className="btn" onClick={onClose}>Cancel</button>
        <button className="btn btn-primary" disabled={!so || !vendor || amount <= 0} onClick={submit}>Create PO {amount > 0 ? `· ${inr(amount)}` : ''}</button>
      </>
    }>
      <div className="field-row">
        <div className="field">
          <label className="field-label">For Sales Order *</label>
          <select className="select" value={so} onChange={e => setSo(e.target.value)} disabled={!!soId}>
            <option value="">Pick SO…</option>
            {state.sales_orders.filter(s => !['Closed', 'Cancelled'].includes(s.status)).map(s => <option key={s.id} value={s.id}>{s.so_no} · {s.status}</option>)}
          </select>
        </div>
        <div className="field">
          <label className="field-label">Vendor *</label>
          <select className="select" value={vendor} onChange={e => setVendor(e.target.value)}>
            <option value="">Pick vendor…</option>
            {state.vendors.map(v => <option key={v.id} value={v.id}>{v.name} · ★ {v.rating}</option>)}
          </select>
        </div>
      </div>
      <div className="field mt-2">
        <label className="field-label">Expected delivery</label>
        <input type="date" className="input mono" value={expected} onChange={e => setExpected(e.target.value)} style={{ width: 180 }}/>
      </div>

      {items.length > 0 ? (
        <div className="card mt-2"><div className="card-body flush">
          <table className="t">
            <thead><tr><th>Item</th><th className="num">Qty</th><th className="num">Rate ₹</th><th className="num">Amount</th></tr></thead>
            <tbody>
              {items.map((it, i) => {
                const p = getProduct(it.product_id) || { name: it.product_id, code: it.product_id };
                const cands = candidatesFor(it.product_id);
                const combine = it.combine || [];
                const total = rowTotalQty(it);
                return (
                  <Fragment key={it.product_id}>
                  <tr>
                    <td>
                      {p.name}<div className="tiny muted mono">{p.code}</div>
                      {cands.length > 0 && (
                        <button type="button" className="btn btn-ghost btn-sm" style={{ marginTop: 2, height: 20, fontSize: 11 }}
                          onClick={() => setCombineOpen(m => ({ ...m, [it.product_id]: !combineOpen[it.product_id] }))}>
                          <Icon name="arrowLeftRight" size={11}/>{combine.length > 0 ? `combining ${combine.length} other SO(s)` : `${cands.length} other SO(s) also need this — combine?`}
                        </button>
                      )}
                    </td>
                    <td className="num"><input type="number" className="input mono" min="0" value={it.qty} onChange={e => setItem(i, { qty: parseInt(e.target.value) || 0 })} style={{ width: 64, textAlign: 'right', height: 24 }}/></td>
                    <td className="num"><input type="number" className="input mono" min="0" value={it.rate} onChange={e => setItem(i, { rate: parseInt(e.target.value) || 0 })} style={{ width: 90, textAlign: 'right', height: 24 }}/></td>
                    <td className="num">{inr(total * (it.rate || 0))}{combine.length > 0 && <div className="tiny muted">{total} total</div>}</td>
                  </tr>
                  {combineOpen[it.product_id] && cands.length > 0 && (
                    <tr><td colSpan={4} style={{ padding: 0 }}>
                      <div style={{ padding: '6px 8px', background: 'var(--bg-subtle)', borderRadius: 4, marginTop: 2, marginBottom: 2 }}>
                        <div className="tiny muted" style={{ marginBottom: 4 }}>Combine this line into the same PO, for:</div>
                        {cands.map(c => {
                          const on = combine.some(x => x.so_id === c.so_id);
                          const row = combine.find(x => x.so_id === c.so_id);
                          return (
                            <div key={c.so_id} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '2px 0' }}>
                              <input type="checkbox" checked={on} onChange={() => toggleCombine(i, c.so_id, c.outstanding)}/>
                              <span className="tiny mono" style={{ width: 130 }}>{c.so_no}</span>
                              <span className="tiny muted">needs {c.outstanding}</span>
                              {on && <input type="number" min="0" className="input mono" value={row.qty} onChange={e => setCombineQty(i, c.so_id, e.target.value)} style={{ width: 64, height: 24, textAlign: 'right', marginLeft: 'auto' }}/>}
                            </div>
                          );
                        })}
                      </div>
                    </td></tr>
                  )}
                  </Fragment>
                );
              })}
            </tbody>
            <tfoot><tr><td colSpan="3" className="right small">Total {needsMD && <span style={{ color: 'var(--warning)' }}>· &gt; ₹5L needs MD</span>}</td><td className="num"><strong>{inr(amount)}</strong></td></tr></tfoot>
          </table>
        </div></div>
      ) : <div className="empty mt-2">{so ? 'This SO has no components' : 'Pick an SO to load its components'}</div>}
    </Modal>
  );
}
window.CreateVendorPOModal = CreateVendorPOModal;

// The matching engine behind Bulk PO's SO picker: of a set of open SOs,
// which ones still need (netted against what each already has on its own
// PO — soOutstandingProcurement, the same function every other combine
// entry point reads) something at least one OTHER SO in the set also needs.
// A reverse index (item -> which SOs need it) turns "does this SO share
// anything with another" into a lookup, not an O(n²) scan per SO, so it
// stays cheap enough to run on the whole list on every render. Pure and
// read-only — nothing here writes anything.
function soShareIndex(state, openSOs) {
  const outstandingBySO = {};
  openSOs.forEach(s => { outstandingBySO[s.id] = window.soOutstandingProcurement ? window.soOutstandingProcurement(state, s) : {}; });
  const sosByProduct = {};
  openSOs.forEach(s => { Object.keys(outstandingBySO[s.id] || {}).forEach(pid => { (sosByProduct[pid] = sosByProduct[pid] || new Set()).add(s.id); }); });
  const counts = {};
  openSOs.forEach(s => { counts[s.id] = Object.keys(outstandingBySO[s.id] || {}).filter(pid => sosByProduct[pid].size > 1).length; });
  return { outstandingBySO, counts };
}
window.soShareIndex = soShareIndex;

// ===== Bulk PO — one vendor PO against SEVERAL Sales Orders at once =====
// CreateVendorPOModal above is SO-first: pick one, then an item's own
// "combine?" badge offers whoever else needs it. This is the other way
// round — Purchase already knows which SOs share a need and wants to pick
// all of them up front, then see one merged shopping list to order from in
// a single PO. Same primitive underneath (so_alloc via buildComboPOItems /
// createComboVendorPO) — only how the SOs get picked differs.
function BulkVendorPOModal({ onClose }) {
  const { state, mutate, getSO, getProduct, getVendor, getCustomer } = useStore();
  const toast = useToast();
  const [picked, setPicked] = React.useState({});    // so.id -> true
  const [soSearch, setSoSearch] = React.useState('');
  const [vendor, setVendor] = React.useState('');
  const [itemSearch, setItemSearch] = React.useState('');
  const [expected, setExpected] = React.useState(() => { const d = new Date(TODAY); d.setDate(d.getDate() + 7); return d.toISOString().slice(0, 10); });
  const [qtyOverride, setQtyOverride] = React.useState({});    // "pid|so.id" -> qty
  const [rateOverride, setRateOverride] = React.useState({});  // pid -> rate

  const openSOs = state.sales_orders.filter(s => !['Closed', 'Cancelled'].includes(s.status));
  const { outstandingBySO: outstandingByAllSO, counts: shareCounts } = soShareIndex(state, openSOs);
  const sharedCountFor = (soId) => shareCounts[soId] || 0;

  // SOs that share something with another open SO surface first — that is
  // the whole point of this screen, so the best candidates to combine should
  // never require scrolling to find. Stable regardless of what is ticked,
  // since sharing is a fact about the SO, not about the current selection.
  const visibleSOs = openSOs
    .filter(s => {
      if (!soSearch.trim()) return true;
      const cust = getCustomer(s.customer_id);
      return `${s.so_no} ${cust ? cust.name : ''}`.toLowerCase().includes(soSearch.trim().toLowerCase());
    })
    .slice()
    .sort((a, b) => sharedCountFor(b.id) - sharedCountFor(a.id) || String(a.so_no).localeCompare(String(b.so_no)));

  const togglePick = (id) => setPicked(m => ({ ...m, [id]: !m[id] }));
  const pickedSOs = openSOs.filter(s => picked[s.id]);
  const pids = Array.from(new Set(pickedSOs.flatMap(s => Object.keys(outstandingByAllSO[s.id] || {}))));

  const qtyFor = (pid, soId) => {
    const key = pid + '|' + soId;
    if (qtyOverride[key] != null) return qtyOverride[key];
    return (outstandingByAllSO[soId] && outstandingByAllSO[soId][pid]) || 0;
  };
  const setQty = (pid, soId, v) => setQtyOverride(m => ({ ...m, [pid + '|' + soId]: Math.max(0, Number(v) || 0) }));
  const rateFor = (pid) => {
    if (rateOverride[pid] != null) return rateOverride[pid];
    const p = getProduct(pid);
    const vendorRate = vendor && window.vendorUnitPrice ? window.vendorUnitPrice(vendor, p) : 0;
    return vendorRate || (p ? (p.buy || 0) : 0);
  };
  const setRate = (pid, v) => setRateOverride(m => ({ ...m, [pid]: Math.max(0, Number(v) || 0) }));

  const rows = pids
    .map(pid => {
      const bySO = pickedSOs.map(s => ({ so_id: s.id, so_no: s.so_no, qty: qtyFor(pid, s.id) }));
      const contributing = bySO.filter(b => b.qty > 0.0001).length;
      return { product_id: pid, rate: rateFor(pid), bySO, contributing, shared: contributing > 1 };
    })
    .map(r => ({ ...r, total: r.bySO.reduce((a, b) => a + b.qty, 0) }))
    .filter(r => r.total > 0.0001)
    .filter(r => !itemSearch.trim() || (getProduct(r.product_id)?.name || r.product_id).toLowerCase().includes(itemSearch.trim().toLowerCase()))
    // Shared lines — the actual reason to combine — read first; single-SO
    // lines (still useful to see, still part of the same PO) trail after.
    .sort((a, b) => (b.shared - a.shared) || (b.contributing - a.contributing));

  const sharedRowCount = rows.filter(r => r.shared).length;
  const amount = rows.reduce((s, r) => s + r.total * r.rate, 0);
  const needsMD = amount > (state.config.vendor_po_md_threshold ?? 500000);

  const submit = () => {
    if (pickedSOs.length === 0 || !vendor) { toast('Tick at least one SO and pick a vendor'); return; }
    const [primary, ...rest] = pickedSOs;
    const items = pids.map(pid => ({
      product_id: pid, rate: rateFor(pid),
      qty: qtyFor(pid, primary.id),
      combine: rest.map(s => ({ so_id: s.id, qty: qtyFor(pid, s.id) })),
    }));
    const result = createComboVendorPO({ state, mutate, getVendor, getSO, primarySoId: primary.id, vendorId: vendor, items, expected });
    if (!result) { toast('Nothing to order — every ticked SO is already fully covered'); return; }
    toast(`${result.po.po_no} created${result.linkedSoIds.length > 1 ? ` · combined across ${result.linkedSoIds.length} SOs` : ''}${result.needsMD ? ' · sent to MD' : ''}`, 'success');
    onClose();
  };

  return (
    <Modal title="Bulk PO — one vendor PO for several Sales Orders" onClose={onClose} size="lg" footer={
      <>
        <button className="btn" onClick={onClose}>Cancel</button>
        <button className="btn btn-primary" disabled={pickedSOs.length === 0 || !vendor || amount <= 0} onClick={submit}>
          Create PO {amount > 0 ? `· ${inr(amount)}` : ''}{pickedSOs.length > 1 ? ` · ${pickedSOs.length} SOs` : ''}
        </button>
      </>
    }>
      <div className="tiny muted mb-2" style={{ padding: 10, background: 'var(--accent-bg)', borderRadius: 4 }}>
        SOs marked <span className="badge accent tiny" style={{ verticalAlign: 'middle' }}>shared</span> already
        overlap with another open SO on at least one item — they're listed
        first. Tick any combination; items any of them still need (netted
        against what each already has on its own PO) appear below, one row
        per item with each SO's own quantity, shared ones on top — edit any
        number before creating.
      </div>
      <div className="field mb-2">
        <label className="field-label">Sales Orders * ({pickedSOs.length} picked)</label>
        <input className="input search mb-1" placeholder="Search SO no or customer…" value={soSearch} onChange={e => setSoSearch(e.target.value)}/>
        <div style={{ maxHeight: 180, overflowY: 'auto', border: '1px solid var(--border)', borderRadius: 4, padding: 6 }}>
          {visibleSOs.map(s => {
            const cust = getCustomer(s.customer_id);
            const shared = sharedCountFor(s.id);
            return (
              <label key={s.id} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '4px 2px', cursor: 'pointer', background: picked[s.id] ? 'var(--bg-subtle)' : 'transparent', borderRadius: 3 }}>
                <input type="checkbox" checked={!!picked[s.id]} onChange={() => togglePick(s.id)}/>
                <span className="tiny mono">{s.so_no}</span>
                <span className="tiny muted">{cust ? cust.name : ''} · {s.status}</span>
                <div className="grow"/>
                {shared > 0 && <span className="badge accent tiny" title={`Shares ${shared} item(s) with at least one other open SO`}>{shared} shared</span>}
              </label>
            );
          })}
          {visibleSOs.length === 0 && <div className="tiny muted">No matching Sales Orders.</div>}
        </div>
      </div>
      <div className="field-row">
        <div className="field">
          <label className="field-label">Vendor *</label>
          <select className="select" value={vendor} onChange={e => setVendor(e.target.value)}>
            <option value="">Pick vendor…</option>
            {state.vendors.map(v => <option key={v.id} value={v.id}>{v.name} · ★ {v.rating}</option>)}
          </select>
        </div>
        <div className="field">
          <label className="field-label">Expected delivery</label>
          <input type="date" className="input mono" value={expected} onChange={e => setExpected(e.target.value)} style={{ width: 180 }}/>
        </div>
      </div>

      {pickedSOs.length === 0 ? (
        <div className="empty mt-2">Tick at least one SO above to see what it needs.</div>
      ) : rows.length === 0 ? (
        <div className="empty mt-2">Every ticked SO is already fully covered by its own vendor PO(s) — nothing left to order.</div>
      ) : (
        <div className="card mt-2"><div className="card-body flush">
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: 8, flexWrap: 'wrap' }}>
            <input className="input search" placeholder="Filter items…" value={itemSearch} onChange={e => setItemSearch(e.target.value)} style={{ width: 220 }}/>
            <span className="tiny muted">
              {sharedRowCount > 0
                ? <><strong style={{ color: 'var(--accent)' }}>{sharedRowCount} shared</strong> item(s) across {pickedSOs.length} ticked SO(s) · {rows.length - sharedRowCount} exclusive to one SO</>
                : <>No item is shared across the ticked SOs yet — every row below belongs to just one of them.</>}
            </span>
          </div>
          <table className="t">
            <thead><tr>
              <th>Item</th>
              {pickedSOs.map(s => <th key={s.id} className="num">{s.so_no}</th>)}
              <th className="num">Rate ₹</th>
              <th className="num">Amount</th>
            </tr></thead>
            <tbody>
              {rows.map(r => {
                const p = getProduct(r.product_id) || { name: r.product_id, code: r.product_id };
                return (
                  <tr key={r.product_id} style={r.shared ? { background: 'var(--accent-bg)' } : undefined}>
                    <td>
                      {p.name}<div className="tiny muted mono">{p.code}</div>
                      {r.shared && <span className="badge accent tiny" style={{ marginTop: 2 }}>shared ×{r.contributing}</span>}
                    </td>
                    {pickedSOs.map(s => {
                      const q = qtyFor(r.product_id, s.id);
                      return (
                        <td key={s.id} className="num">
                          <input type="number" min="0" className="input mono" value={q}
                            onChange={e => setQty(r.product_id, s.id, e.target.value)}
                            style={{ width: 56, textAlign: 'right', height: 24, opacity: q > 0 ? 1 : 0.4 }}/>
                        </td>
                      );
                    })}
                    <td className="num"><input type="number" min="0" className="input mono" value={r.rate} onChange={e => setRate(r.product_id, e.target.value)} style={{ width: 80, textAlign: 'right', height: 24 }}/></td>
                    <td className="num mono">{inr(r.total * r.rate)}</td>
                  </tr>
                );
              })}
            </tbody>
            <tfoot><tr><td colSpan={pickedSOs.length + 2} className="right small">Total {needsMD && <span style={{ color: 'var(--warning)' }}>· &gt; ₹5L needs MD</span>}</td><td className="num"><strong>{inr(amount)}</strong></td></tr></tfoot>
          </table>
        </div></div>
      )}
    </Modal>
  );
}
window.BulkVendorPOModal = BulkVendorPOModal;

// ===== In transit — the gap between "PO issued" and "GRN posted" =====
// Without this, material that has left the vendor but not arrived is invisible:
// the PO reads "Issued" and the godown reads "nothing received", so nobody can
// answer "where is it?". Writing the LR here makes that leg trackable and feeds
// the In-transit column on SCM Tracking.
//
// Stored on vendor_pos.dispatch_info (jsonb) so it needs no new table and no
// migration per field. Only shown when the org workflow asks for it.
function InTransitModal({ po, onClose }) {
  const { mutate, getVendor, getProduct } = useStore();
  const toast = useToast();
  const d = po.dispatch_info || {};
  const v = getVendor(po.vendor_id);
  const [shippedOn, setShippedOn] = React.useState(d.shipped_on || TODAY);
  const [lr, setLr] = React.useState(d.lr_no || '');
  const [carrier, setCarrier] = React.useState(d.carrier || '');
  const [track, setTrack] = React.useState(d.tracking_url || '');
  const [contact, setContact] = React.useState(d.contact || '');
  const [eta, setEta] = React.useState(d.eta || '');
  const [notes, setNotes] = React.useState(d.notes || '');
  // Quantity-wise: a vendor may ship a PO in more than one lorry.
  const [qty, setQty] = React.useState(() => {
    const prev = {};
    ((d.items) || []).forEach(it => { prev[it.product_id] = it.qty; });
    const init = {};
    (po.items || []).forEach(it => { init[it.product_id] = prev[it.product_id] != null ? prev[it.product_id] : it.qty; });
    return init;
  });

  const shippedUnits = (po.items || []).reduce((a, it) => a + (Number(qty[it.product_id]) || 0), 0);
  const orderedUnits = (po.items || []).reduce((a, it) => a + (Number(it.qty) || 0), 0);

  const save = () => {
    if (!lr.trim() && !carrier.trim()) { toast('Enter an LR / docket number or a carrier'); return; }
    if (shippedUnits <= 0) { toast('Nothing to dispatch — enter at least one quantity'); return; }
    const info = {
      shipped_on: shippedOn || TODAY,
      lr_no: lr.trim(), carrier: carrier.trim(), tracking_url: track.trim(),
      contact: contact.trim(), eta: eta || '', notes: notes.trim(),
      items: (po.items || []).map(it => ({ product_id: it.product_id, qty: Number(qty[it.product_id]) || 0 })).filter(x => x.qty > 0),
      updated_at: new Date().toISOString(),
    };
    mutate(st => ({
      ...st,
      vendor_pos: st.vendor_pos.map(x => x.id === po.id
        ? { ...x, dispatch_info: info, status: x.status === 'Issued' ? 'In Transit' : x.status }
        : x),
      notifications: [{
        id: 'n-transit-' + Date.now(), kind: 'po',
        text: `${po.po_no}: ${shippedUnits} unit(s) dispatched by ${v ? v.name : 'vendor'}${lr.trim() ? ' · LR ' + lr.trim() : ''}${eta ? ' · ETA ' + fmtDate(eta) : ''}`,
        date: TODAY, read: false, role: wfReceiving().requesterLabel,
      }, ...st.notifications],
    }), {
      action: 'po-in-transit', entity: 'VendorPO', entity_id: po.id,
      detail: `${shippedUnits}/${orderedUnits} unit(s) in transit${lr.trim() ? ' · LR ' + lr.trim() : ''}${carrier.trim() ? ' · ' + carrier.trim() : ''}`,
    });
    toast(`Marked in transit · ${shippedUnits} unit(s)${lr.trim() ? ' · LR ' + lr.trim() : ''}`, 'success');
    onClose();
  };

  const clear = () => {
    mutate(st => ({ ...st, vendor_pos: st.vendor_pos.map(x => x.id === po.id ? { ...x, dispatch_info: {}, status: x.status === 'In Transit' ? 'Issued' : x.status } : x) }),
      { action: 'po-in-transit-clear', entity: 'VendorPO', entity_id: po.id, detail: 'In-transit details cleared' });
    toast('In-transit details cleared', '');
    onClose();
  };

  return (
    <Modal title={`In transit — ${po.po_no}`} onClose={onClose} size="lg" footer={
      <>
        {(po.dispatch_info && Object.keys(po.dispatch_info).length > 0) && <button className="btn" onClick={clear}>Clear</button>}
        <button className="btn" onClick={onClose}>Cancel</button>
        <button className="btn btn-primary" onClick={save}><Icon name="truck" size={13}/>Save transit details</button>
      </>
    }>
      <div className="tiny muted mb-2">
        Material has left <strong>{v ? v.name : 'the vendor'}</strong> but has not been received yet.
        Recording it here shows the units as <strong>In transit</strong> on SCM Tracking until a GRN is posted.
      </div>
      <div className="field-row">
        <div className="field"><label className="field-label">Dispatched on</label>
          <input className="input" type="date" value={shippedOn} onChange={e => setShippedOn(e.target.value)}/></div>
        <div className="field"><label className="field-label">Expected arrival (ETA)</label>
          <input className="input" type="date" value={eta} onChange={e => setEta(e.target.value)}/></div>
        <div className="field"><label className="field-label">LR / docket / AWB no.</label>
          <input className="input mono" value={lr} onChange={e => setLr(e.target.value)} placeholder="e.g. TCI-MUM-99821"/></div>
        <div className="field"><label className="field-label">Carrier / transporter</label>
          <input className="input" value={carrier} onChange={e => setCarrier(e.target.value)} placeholder="e.g. TCI Express"/></div>
        <div className="field"><label className="field-label">Tracking link</label>
          <input className="input" value={track} onChange={e => setTrack(e.target.value)} placeholder="https://…"/></div>
        <div className="field"><label className="field-label">Driver / contact no.</label>
          <input className="input mono" value={contact} onChange={e => setContact(e.target.value)} placeholder="e.g. 98200 00000"/></div>
      </div>
      <div className="field"><label className="field-label">Notes</label>
        <input className="input" value={notes} onChange={e => setNotes(e.target.value)} placeholder="Part shipment, damaged carton, etc."/></div>

      <div className="card mt-2">
        <div className="card-header">
          <h3 className="card-title">What is on this lorry</h3>
          <div className="tiny muted">{shippedUnits} of {orderedUnits} ordered unit(s) — reduce a line for a part shipment</div>
        </div>
        <div className="card-body flush">
          <table className="t">
            <thead><tr><th>Item</th><th className="num">Ordered</th><th className="num">Dispatched</th></tr></thead>
            <tbody>
              {(po.items || []).map((it, i) => {
                const pr = getProduct(it.product_id);
                return (
                  <tr key={i}>
                    <td>{pr ? pr.name : it.product_id}<div className="tiny muted mono">{pr ? pr.code : ''}</div></td>
                    <td className="num">{qty(it.qty)}</td>
                    <td className="num" style={{ width: 120 }}>
                      <input className="input num" type="number" min="0" max={it.qty} value={qty[it.product_id] != null ? qty[it.product_id] : ''}
                        onChange={e => {
                          const n = Math.max(0, Math.min(Number(it.qty) || 0, Number(e.target.value) || 0));
                          setQty(q => ({ ...q, [it.product_id]: n }));
                        }}/>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      </div>
    </Modal>
  );
}

window.CreateVendorPOModal = CreateVendorPOModal;
window.RFQList = RFQList;
window.VendorPOList = VendorPOList;
window.VendorPODetail = VendorPODetail;
window.InTransitModal = InTransitModal;
window.GRNList = GRNList;
window.GRNDetail = GRNDetail;
window.GRNNew = GRNNew;
window.ThreeWayMatchList = ThreeWayMatchList;
window.ThreeWayMatchDetail = ThreeWayMatchDetail;
