// ─── Doc-kit: tabla de ítems (3/4) ───────────────────────────────────
// ItemsTable: la tabla de ítems editable de todo documento (Presupuesto/OC/
// Remito/Factura/Recibo + Finanzas + Compras/Aprobaciones + Inventario).
// InventoryCombobox: combobox genérico de tipeahead contra window.INVENTARIO,
// parametrizado por `variant` ('desc' | 'sku') — fusiona lo que antes eran
// InventoryDescCell/InventorySkuCell (screens-docs.jsx), ~130 líneas casi
// idénticas (mismo hook de posicionamiento del popover, mismo listener de
// scroll/resize/click-outside, mismo patrón de portal `.inv-pop`), que
// diferían solo en qué completan y cómo renderizan la sugerencia. Extraído de
// screens-docs.jsx en P4. Expone a window: ItemsTable, InventoryCombobox
// (UnitToggle es interno, sin consumidores fuera de este archivo). Consume
// window.INVENTARIO, window.MicosItemsMath (doc-itemmath.js), window.SUGGEST_LIMIT
// (core/bundle-config.js), formatMoney (dominio/formato.js). El patrón `.inv-pop`
// de portal (estilos en styles.css) vive acá.
//
// P17b (recorrido §4 — selectores de ítems del ERP, tocado ACÁ para que
// aplique a todo caller, regla de la casa "reusar/ampliar, no crear"):
// (a) popover al DOBLE de ancho mínimo (200→400 variante sku, 360→720
// variante desc — antes se quedaba angosto con descripciones largas);
// (b) el nombre del producto ya NO trunca con ellipsis: hace wrap y sigue
// abajo (`.inv-pop__desc` en styles.css, `white-space`/`overflow`/
// `text-overflow` sacados); (c) por default NUNCA se ofrece un ítem sin
// stock — el `matches` descarta `stock <= 0` antes de recortar a
// SUGGEST_LIMIT. RESUELTO el arbitraje P17b (ver p17b-reporte.md "Notas de
// merge"): este combobox es el MISMO en Presupuesto/Remito/Factura (venta,
// filtra sin-stock) Y en Orden de Compra/Compras (procurement, donde stock=0
// es EXACTAMENTE lo que se busca reponer). Se agregó el prop opt-out
// `mostrarSinStock` (default false = venta sigue filtrando); OC y Compras lo
// prenden en su <ItemsTable>. Fijado en ambos sentidos por
// tests/e2e/flujos/inventario-sin-stock-procurement.spec.ts.
//
// P19: selector "%"/"$" por columna en Ganancia/Bonificación — ver comentario
// de "Modelo de precio real" abajo (zona separada de InventoryCombobox, sin
// solape de lógica con lo de P17b arriba).
//
// FIX P24 (03/07/2026, diagnóstico del paquete de inventario): los dos `onPick` (variant sku
// y desc) hacían `onItemChange(i, 'unitario', p.costoUnitario)` sin guarda — con
// `costo_unitario` vacío en 600/601 productos reales, elegir una SUGERENCIA del combobox
// (en vez de tipear) zeraba en silencio el precio de la línea (`unitario: null` →
// `MicosItemsMath.normalize` → `num(null)=0` → subtotal $0,00). Ahora solo pisa `unitario` si
// el producto tiene costo real (`> 0`); si no, deja el valor que el usuario ya haya tipeado.

// ─── Combobox genérico de Inventario (portal, .inv-pop) ────────────
// variant='desc': completa descripción (+ sugiere sku/unitario al elegir).
// variant='sku':  completa código (+ sugiere descripción/unitario al elegir).
// En ambos casos `onPick(producto)` deja que el caller decida qué campos
// completar — acá solo se arma la sugerencia y el input de la celda.
function InventoryCombobox({ value, onChange, onPick, variant = 'desc', mostrarSinStock = false }) {
  const isSku = variant === 'sku';
  const [open, setOpen] = React.useState(false);
  const [rect, setRect] = React.useState(null);
  const inputRef = React.useRef(null);
  const inv = window.INVENTARIO || [];
  const query = (value || '').trim().toLowerCase();
  const matches = inv
    .filter((p) => (`${p.sku} ${p.descripcion} ${p.categoria || ''}`).toLowerCase().includes(query))
    // Por default el selector de inventario NUNCA ofrece un ítem sin stock (venta: no vendés lo
    // que no tenés). Excepción con `mostrarSinStock` (opt-out del arbitraje P17b, ver header):
    // en procurement (Orden de Compra / Compras) stock=0 es JUSTO lo que se repone, así que esos
    // 2 callers lo prenden y ahí sí se ofrecen ítems agotados.
    .filter((p) => mostrarSinStock || (p.stock || 0) > 0)
    .slice(0, (window.SUGGEST_LIMIT || 8));

  const place = () => { if (inputRef.current) setRect(inputRef.current.getBoundingClientRect()); };
  React.useEffect(() => {
    if (!open) return;
    place();
    const close = () => setOpen(false);
    // Al scrollear, reposicionar el popover (no cerrarlo) salvo que el scroll
    // ocurra dentro del propio popover (la lista tiene su propio scroll).
    const onScroll = (e) => {
      if (e.target && e.target.closest && e.target.closest('.inv-pop')) return;
      place();
    };
    window.addEventListener('scroll', onScroll, true);
    window.addEventListener('resize', close);
    const onDoc = (e) => {
      const inPop = e.target.closest && e.target.closest('.inv-pop');
      if (inputRef.current && !inputRef.current.contains(e.target) && !inPop) setOpen(false);
    };
    document.addEventListener('mousedown', onDoc);
    return () => {
      window.removeEventListener('scroll', onScroll, true);
      window.removeEventListener('resize', close);
      document.removeEventListener('mousedown', onDoc);
    };
  }, [open]);

  const pop = (open && rect && matches.length > 0) ? ReactDOM.createPortal(
    <div className="inv-pop" style={{ position: 'fixed', top: rect.bottom + 5, left: rect.left, width: Math.max(rect.width, isSku ? 400 : 720), maxHeight: isSku ? 260 : 280, overflowY: 'auto' }}>
      <p className="inv-pop__head">{isSku ? 'Inventario · elegí un código' : 'Inventario · elegí un producto'}</p>
      {matches.map((p) => (
        <button
          key={p.id}
          type="button"
          className="inv-pop__row"
          onMouseDown={(e) => { e.preventDefault(); onPick(p); setOpen(false); }}>
          <span className="inv-pop__sku">{p.sku}</span>
          {!isSku && (
            <>
              <span className="inv-pop__desc">{p.descripcion}</span>
              <span className="inv-pop__meta">
                <span className="inv-pop__price">{formatMoney(p.costoUnitario)}</span>
                <span className="inv-pop__stock">stock {p.stock}</span>
              </span>
            </>
          )}
        </button>
      ))}
    </div>, document.body) : null;

  return (
    <div style={{ position: 'relative' }}>
      <input
        ref={inputRef}
        value={value || ''}
        onChange={(e) => { onChange(e.target.value); if (!open) setOpen(true); }}
        onFocus={() => setOpen(true)}
        className={isSku
          ? 'edit-input edit-input--cell w-full font-mono text-[12px] font-bold uppercase tracking-wide'
          : 'edit-input edit-input--cell w-full font-bold leading-snug'}
        placeholder={isSku ? 'ID' : 'Descripción o buscar en inventario…'}
        style={{ color: isSku ? 'var(--accent)' : 'var(--foreground)' }}
      />
      {pop}
    </div>
  );
}

// Modelo de precio real (window.MicosItemsMath.normalize, doc-itemmath.js — fuente única):
//   unitario    = precio de COMPRA (costo)
//   gananciaPct = ganancia en % SOBRE el costo → venta unitaria = unitario × (1 + gananciaPct/100)
//   bonifPct    = bonificación en % sobre la base de venta
//   ivaPct      = alícuota IVA (se muestra el monto $ debajo)
//   base línea  = venta unitaria × cantidad ; IVA sobre la base (antes de bonif)
//   subtotal    = base + IVA − bonificación
// (El comentario viejo de acá describía un modelo "ganancia en $" que `doc-itemmath.js` ya no
// usa como fuente de verdad — corregido P19, ver nota de abajo.)
//
// P19 (recorrido §5): selector "%"/"$" en Ganancia y Bonificación — en vez del símbolo fijo de
// unidad, un toggle chico por COLUMNA (aplica a todas las filas de ESTA tabla; no por fila: un
// documento se carga con un solo criterio de entrada consistente, más fácil de leer). Default
// Ganancia = "$", Bonificación = "%" (regla de Fran: "algunos redondean en plata"). El campo
// que NO es el modo activo se muestra abajo, en gris, como valor derivado — invertido según el
// modo. Clave de diseño: el modo NUNCA agrega un campo de estado nuevo — `onItemChange` sigue
// emitiendo siempre `gananciaPct`/`bonifPct` (los mismos que ya persisten/leen
// MicosItemsMath.normalize/migrate y MicosSyncMargenNotif), convertidos desde el $ tipeado. Así
// ningún caller de ItemsTable (7 pantallas de documento + Compras + Finanzas) necesita cambios:
// reciben el mismo shape de siempre. La conversión usa el MISMO costUnit/base de `normalize()`
// (no se recalcula aparte) para que %→$→% sea exacta sin drift de centavos. NO toca la deuda
// conocida "presupuesto_item sin columna ganancia_pct" (paquetes/07-docs-reporte.md #1, decisión
// Fran: migración post-refactor) — sigue exactamente igual de bien/mal que antes: gananciaPct
// se recalcula desde costo+bonif al reabrir un documento ya guardado, con o sin este selector.
function UnitToggle({ mode, onChange, title }) {
  return (
    <div
      className="inline-flex items-center gap-0.5 rounded-md"
      style={{ border: '1px solid var(--border)', padding: 1, background: 'var(--surface)' }}
      title={title}>
      {['%', '$'].map((u) => (
        <button
          key={u}
          type="button"
          onClick={() => onChange(u)}
          className="cursor-pointer"
          style={{
            padding: '1px 5px',
            borderRadius: 3,
            fontSize: 10,
            fontWeight: 800,
            lineHeight: '13px',
            border: 'none',
            background: mode === u ? 'var(--accent)' : 'transparent',
            color: mode === u ? 'var(--accent-foreground)' : 'var(--muted-foreground)',
          }}>
          {u}
        </button>
      ))}
    </div>
  );
}

// Edit mode: pass `editing`, `onItemChange(idx, field, value)`, `onItemRemove(idx)`, `onAdd()`.
// `subtotalEditable` (DECISIÓN A2, 07/07/2026, opt-in — default false, no rompe a nadie más):
// convierte la celda de Subtotal en un input; al tipear dispara
// `onItemChange(idx, 'subtotal', valor)` para que el CALLER decida qué hacer (hoy solo
// Presupuesto lo pasa y aplica `MicosItemsMath.applySubtotalInverso` en su `patchItem` — OC/
// Remito/Factura/Compras/Finanzas no lo pasan, su columna de Subtotal sigue de solo lectura).
function ItemsTable({ items, showBonif = false, showIva = false, showGanancia = false, editing = false, onItemChange, onItemRemove, onAdd, flash = 0, mostrarSinStock = false, subtotalEditable = false }) {
  // P19: modo de entrada por columna (ver comentario de arriba) — un solo estado por tabla,
  // no por fila. Default Ganancia "$" / Bonificación "%" (recorrido §5).
  const [gananciaModo, setGananciaModo] = React.useState('$');
  const [bonifModo, setBonifModo] = React.useState('%');
  // DECISIÓN A2: buffer local del texto que se está tipeando en el Subtotal, por fila (índice).
  // No se controla el input directo desde el subtotal DERIVADO (dn.base+iva−bonif) porque ese
  // valor se recalcula en cada tecla a partir de lo que el usuario ya escribió — si el input
  // reflejara siempre el derivado, pelearía con la tipeada (el cursor "salta"). Se commitea
  // recién al salir del campo (blur/Enter): ahí sí se dispara el cálculo inverso real.
  const [subtotalDraft, setSubtotalDraft] = React.useState({});

  // Build dynamic grid template — generous column widths for readability
  const baseCols = ['140px', '1fr', '70px', '130px']; // SKU | Desc | Cant | Unit
  if (showGanancia) baseCols.push('148px');
  if (showBonif) baseCols.push('138px');
  if (showIva) baseCols.push('104px');
  baseCols.push('150px'); // Subtotal
  if (editing) baseCols.push('36px'); // delete btn
  const template = baseCols.join(' ');
  let minWidth = 700;
  if (showBonif) minWidth += 130;
  if (showIva) minWidth += 110;
  if (showGanancia) minWidth += 140;
  if (editing) minWidth += 40;

  return (
    <div key={flash || 'noflash'} className={flash ? 'autofill-flash' : ''}>
      {/* Table */}
      <div className="rounded-xl overflow-hidden" style={{ border: '1px solid color-mix(in oklab, var(--border) 55%, transparent)' }}>
        <div style={{ overflowX: 'auto' }}>
          <div style={{ minWidth: `${minWidth}px` }}>
            <div
              className="grid gap-4 px-5 py-4 text-[10.5px] font-bold uppercase tracking-[0.12em]"
              style={{
                gridTemplateColumns: template,
                color: 'var(--muted-foreground)',
                background: 'color-mix(in oklab, var(--surface) 55%, transparent)',
                borderBottom: '1px solid color-mix(in oklab, var(--border) 55%, transparent)'
              }}>

              <div>ID</div>
              <div>Descripción</div>
              <div className="text-right">Cant.</div>
              <div className="text-right">Unitario</div>
              {showGanancia && (
                <div className="text-right flex items-center justify-end gap-1.5">
                  Ganancia
                  {editing && <UnitToggle mode={gananciaModo} onChange={setGananciaModo} title="Unidad de carga: % sobre costo o $ de la línea" />}
                </div>
              )}
              {showBonif && (
                <div className="text-right flex items-center justify-end gap-1.5">
                  Bonif.
                  {editing && <UnitToggle mode={bonifModo} onChange={setBonifModo} title="Unidad de carga: % sobre la base o $ de la línea" />}
                </div>
              )}
              {showIva && <div className="text-right">IVA</div>}
              <div className="text-right">Subtotal</div>
            </div>
            {items.map((it, i) =>
            <div
              key={i}
              className={`grid gap-4 px-5 py-5 items-start text-[14px] ${editing ? 'item-row--editing' : ''}`}
              style={{
                gridTemplateColumns: template,
                borderBottom: i < items.length - 1 || editing ? '1px solid color-mix(in oklab, var(--border) 40%, transparent)' : 'none'
              }}>

                <div className="font-mono text-[12px] font-bold tracking-wide uppercase truncate" style={{ color: 'var(--accent)' }}>
                  {editing ? (
                    <InventoryCombobox
                      variant="sku"
                      mostrarSinStock={mostrarSinStock}
                      value={it.sku || it.codigo || ''}
                      onChange={(v) => onItemChange(i, 'sku', v)}
                      onPick={(p) => {
                        onItemChange(i, 'sku', p.sku);
                        onItemChange(i, 'descripcion', p.descripcion);
                        // P24 (03/07/2026): `costoUnitario` está vacío en la enorme mayoría del
                        // catálogo real (600/601 productos, verificado docker exec psql) — pisar
                        // `unitario` con null/0 acá zeraba en silencio el precio de la línea
                        // (MicosItemsMath.normalize → num(null)=0 → subtotal $0) apenas el
                        // usuario elegía una SUGERENCIA en vez de tipear (bug encontrado en el
                        // diagnóstico de este paquete). Si no hay costo real cargado, se deja el
                        // `unitario` que el usuario ya haya tipeado (no se inventa un precio).
                        if (p.costoUnitario != null && Number(p.costoUnitario) > 0) onItemChange(i, 'unitario', p.costoUnitario);
                      }}
                    />
                  ) : (it.sku || it.codigo)}
                </div>
                <div className="min-w-0">
                  {editing ? (
                    <InventoryCombobox
                      variant="desc"
                      mostrarSinStock={mostrarSinStock}
                      value={it.descripcion}
                      onChange={(v) => onItemChange(i, 'descripcion', v)}
                      onPick={(p) => {
                        onItemChange(i, 'sku', p.sku);
                        onItemChange(i, 'descripcion', p.descripcion);
                        // Ídem variant="sku" de arriba — no pisar el precio con un costo vacío.
                        if (p.costoUnitario != null && Number(p.costoUnitario) > 0) onItemChange(i, 'unitario', p.costoUnitario);
                      }}
                    />
                  ) : (
                    <p className="font-bold leading-snug" style={{ color: 'var(--foreground)' }}>{it.descripcion}</p>
                  )}
                  {it.lote && !editing &&
                <p className="text-[10.5px] font-bold uppercase tracking-[0.08em] mt-1.5" style={{ color: 'var(--muted-foreground)' }}>
                      Lote · {it.lote}
                    </p>
                }
                </div>
                <div className="text-right font-bold tabular-nums" style={{ color: 'var(--foreground)' }}>
                  {editing ? (
                    <input
                      type="number"
                      value={it.cantidad}
                      onChange={(e) => onItemChange(i, 'cantidad', Number(e.target.value) || 0)}
                      className="edit-input edit-input--cell w-full text-right tabular-nums font-bold"
                      style={{ color: 'var(--foreground)' }}
                    />
                  ) : it.cantidad}
                </div>
                <div className="text-right tabular-nums" style={{ color: 'var(--muted-foreground)' }}>
                  {editing ? (
                    <input
                      type="number"
                      value={it.unitario}
                      onChange={(e) => onItemChange(i, 'unitario', Number(e.target.value) || 0)}
                      className="edit-input edit-input--cell w-full text-right tabular-nums"
                      style={{ color: 'var(--foreground)' }}
                    />
                  ) : formatMoney((window.MicosItemsMath ? window.MicosItemsMath.normalize(it).costUnit : it.unitario))}
                </div>
                {showGanancia && (() => {
                  const dn = window.MicosItemsMath.normalize(it);
                  const gp = dn.gananciaPct;
                  const color = gp == null ? 'var(--muted-foreground)'
                    : (gp > 0 ? (gp >= 35 ? 'var(--success)' : gp >= 25 ? 'var(--warning)' : 'var(--muted-foreground)')
                             : (gp < 0 ? 'var(--danger)' : 'var(--muted-foreground)'));
                  if (editing && gananciaModo === '%') {
                    // Ingresa la GANANCIA en % sobre el costo. Debajo, el valor en $ (línea).
                    const has = it.gananciaPct != null && it.gananciaPct !== '';
                    return (
                      <div className="text-right tabular-nums">
                        <div className="flex items-center justify-end gap-1">
                          <input
                            type="number"
                            value={has ? it.gananciaPct : ''}
                            onChange={(e) => { const r = e.target.value; onItemChange(i, 'gananciaPct', r === '' ? null : (Number(r) || 0)); }}
                            placeholder="0"
                            className="edit-input edit-input--cell flex-1 min-w-0 text-right tabular-nums"
                            style={{ color: 'var(--foreground)' }}
                          />
                          <span className="text-[12px] font-bold" style={{ color: 'var(--muted-foreground)' }}>%</span>
                        </div>
                        <p className="text-[10.5px] font-bold tabular-nums mt-1" style={{ color: 'var(--muted-foreground)' }}>
                          {formatMoney(dn.ganMonto)}
                        </p>
                      </div>
                    );
                  }
                  if (editing) {
                    // P19: modo "$" — ingresa la GANANCIA en $ de la LÍNEA (mismo valor que ya
                    // se mostraba debajo en modo %: dn.ganMonto). Se convierte a gananciaPct con
                    // la MISMA base (dn.costUnit×dn.cant) que usa normalize() para ir de % a $,
                    // así el viaje %→$→% es la inversa exacta (sin drift de centavos) — abajo,
                    // el % equivalente.
                    const hasMonto = it.gananciaPct != null && it.gananciaPct !== '';
                    return (
                      <div className="text-right tabular-nums">
                        <div className="flex items-center justify-end gap-1">
                          <span className="text-[12px] font-bold" style={{ color: 'var(--muted-foreground)' }}>$</span>
                          <input
                            type="number"
                            step="0.01"
                            value={hasMonto ? window.MicosItemsMath._round(dn.ganMonto) : ''}
                            onChange={(e) => {
                              const r = e.target.value;
                              if (r === '') { onItemChange(i, 'gananciaPct', null); return; }
                              const montoLinea = Number(r) || 0;
                              const base = dn.costUnit * dn.cant;
                              onItemChange(i, 'gananciaPct', base > 0 ? (montoLinea / base) * 100 : 0);
                            }}
                            placeholder="0"
                            className="edit-input edit-input--cell flex-1 min-w-0 text-right tabular-nums"
                            style={{ color: 'var(--foreground)' }}
                          />
                        </div>
                        <p className="text-[10.5px] font-bold tabular-nums mt-1" style={{ color: 'var(--muted-foreground)' }}>
                          {`${gp.toFixed(2)}%`}
                        </p>
                      </div>
                    );
                  }
                  return (
                    <div className="text-right">
                      <p className="text-[14px] font-bold tabular-nums" style={{ color }}>{`${gp.toFixed(0)}%`}</p>
                      <p className="text-[11.5px] font-bold tabular-nums mt-0.5" style={{ color: 'var(--muted-foreground)' }}>
                        {formatMoney(dn.ganMonto)}
                      </p>
                    </div>
                  );
                })()}
                {showBonif && (() => {
                  const dn = window.MicosItemsMath.normalize(it);
                  const bp = dn.bonifPct;
                  if (editing && bonifModo === '%') {
                    // Ingresa la BONIFICACIÓN en % sobre la base. Debajo, el valor en $ (línea).
                    const has = it.bonifPct != null && it.bonifPct !== '';
                    return (
                      <div className="text-right tabular-nums">
                        <div className="flex items-center justify-end gap-1">
                          <input
                            type="number"
                            value={has ? it.bonifPct : ''}
                            onChange={(e) => { const r = e.target.value; onItemChange(i, 'bonifPct', r === '' ? null : (Number(r) || 0)); }}
                            placeholder="0"
                            className="edit-input edit-input--cell flex-1 min-w-0 text-right tabular-nums"
                            style={{ color: 'var(--foreground)' }}
                          />
                          <span className="text-[12px] font-bold" style={{ color: 'var(--muted-foreground)' }}>%</span>
                        </div>
                        <p className="text-[10.5px] font-bold tabular-nums mt-1" style={{ color: 'var(--muted-foreground)' }}>
                          {dn.bonifMonto > 0 ? `−${formatMoney(dn.bonifMonto)}` : formatMoney(0)}
                        </p>
                      </div>
                    );
                  }
                  if (editing) {
                    // P19: modo "$" — ingresa la BONIFICACIÓN en $ de la LÍNEA (mismo valor que
                    // ya se mostraba debajo en modo %: dn.bonifMonto). Se convierte a bonifPct
                    // con la MISMA base de venta (dn.base = ventaUnit×cant, YA post-ganancia)
                    // que usa normalize() — mismo criterio que Ganancia arriba, sin drift.
                    const hasMonto = it.bonifPct != null && it.bonifPct !== '';
                    return (
                      <div className="text-right tabular-nums">
                        <div className="flex items-center justify-end gap-1">
                          <span className="text-[12px] font-bold" style={{ color: 'var(--muted-foreground)' }}>$</span>
                          <input
                            type="number"
                            step="0.01"
                            value={hasMonto ? window.MicosItemsMath._round(dn.bonifMonto) : ''}
                            onChange={(e) => {
                              const r = e.target.value;
                              if (r === '') { onItemChange(i, 'bonifPct', null); return; }
                              const montoLinea = Number(r) || 0;
                              const base = dn.base;
                              onItemChange(i, 'bonifPct', base > 0 ? (montoLinea / base) * 100 : 0);
                            }}
                            placeholder="0"
                            className="edit-input edit-input--cell flex-1 min-w-0 text-right tabular-nums"
                            style={{ color: 'var(--foreground)' }}
                          />
                        </div>
                        <p className="text-[10.5px] font-bold tabular-nums mt-1" style={{ color: 'var(--muted-foreground)' }}>
                          {`${bp.toFixed(2)}%`}
                        </p>
                      </div>
                    );
                  }
                  return (
                    <div className="text-right" style={{ color: bp > 0 ? 'var(--foreground)' : 'color-mix(in oklab, var(--muted-foreground) 50%, transparent)' }}>
                      <p className="text-[14px] font-bold tabular-nums">{bp > 0 ? `${bp.toFixed(1)}%` : '—'}</p>
                      {bp > 0 && (
                        <p className="text-[11.5px] font-bold tabular-nums mt-0.5" style={{ color: 'var(--muted-foreground)' }}>−{formatMoney(dn.bonifMonto)}</p>
                      )}
                    </div>
                  );
                })()}
                {showIva && (() => {
                  const dn = window.MicosItemsMath.normalize(it);
                  return (
                    <div className="text-right tabular-nums" style={{ color: 'var(--muted-foreground)' }}>
                      {editing ? (
                        <select
                          value={it.ivaPct ?? 21}
                          onChange={(e) => onItemChange(i, 'ivaPct', Number(e.target.value))}
                          className="edit-input edit-input--cell w-full text-right tabular-nums"
                          style={{ color: 'var(--foreground)' }}>
                          <option value={0}>0%</option>
                          <option value={10.5}>10,5%</option>
                          <option value={21}>21%</option>
                        </select>
                      ) : (
                        <p className="text-[14px] font-bold tabular-nums" style={{ color: 'var(--foreground)' }}>{`${String(dn.ivaPct).replace('.', ',')}%`}</p>
                      )}
                      <p className="text-[10.5px] font-bold tabular-nums mt-1" style={{ color: 'var(--muted-foreground)' }}>
                        {formatMoney(dn.ivaMonto)}
                      </p>
                    </div>
                  );
                })()}
                {(() => {
                  const dn = window.MicosItemsMath.normalize(it);
                  // Subtotal ítem = base (unit+ganancia)×cant + IVA − bonificación
                  const lineSubtotal = dn.base + (showIva ? dn.ivaMonto : 0) - (showBonif ? dn.bonifMonto : 0);
                  if (editing && subtotalEditable) {
                    // DECISIÓN A2: input libre con buffer local (ver comentario de arriba) —
                    // muestra lo tipeado mientras se edita, o el subtotal derivado si no hay
                    // edición en curso en esta fila.
                    const draft = subtotalDraft[i];
                    const shown = draft != null ? draft : (lineSubtotal ? String(window.MicosItemsMath._round(lineSubtotal)) : '');
                    const commit = () => {
                      setSubtotalDraft((d) => { const n = { ...d }; delete n[i]; return n; });
                      if (draft == null || draft === '') return;
                      onItemChange(i, 'subtotal', draft);
                    };
                    return (
                      <input
                        type="number"
                        step="0.01"
                        value={shown}
                        onChange={(e) => setSubtotalDraft((d) => ({ ...d, [i]: e.target.value }))}
                        onBlur={commit}
                        onKeyDown={(e) => { if (e.key === 'Enter') e.currentTarget.blur(); }}
                        className="edit-input edit-input--cell w-full text-right tabular-nums font-extrabold"
                        style={{ color: 'var(--foreground)' }}
                        title="Editá el subtotal: se recalculan IVA → precio unitario → ganancia"
                      />
                    );
                  }
                  return (
                    <div className="text-right font-extrabold tabular-nums" style={{ color: 'var(--foreground)' }}>{formatMoney(lineSubtotal)}</div>
                  );
                })()}
                {editing &&
                  <button
                    onClick={() => onItemRemove(i)}
                    className="flex items-center justify-center rounded-md transition-colors cursor-pointer"
                    style={{
                      width: 28,
                      height: 28,
                      color: 'var(--destructive)',
                      background: 'color-mix(in oklab, var(--destructive) 8%, transparent)',
                    }}
                    onMouseEnter={(e) => { e.currentTarget.style.background = 'color-mix(in oklab, var(--destructive) 18%, transparent)'; }}
                    onMouseLeave={(e) => { e.currentTarget.style.background = 'color-mix(in oklab, var(--destructive) 8%, transparent)'; }}
                    title="Eliminar ítem">
                    <IconTrash size={14} />
                  </button>
                }
              </div>
            )}

            {editing &&
              <button
                onClick={onAdd}
                className="flex items-center justify-center gap-2 w-full px-5 py-4 text-[13px] font-bold uppercase tracking-[0.1em] transition-colors cursor-pointer"
                style={{ color: 'var(--accent)' }}
                onMouseEnter={(e) => e.currentTarget.style.background = 'color-mix(in oklab, var(--accent) 7%, transparent)'}
                onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
                <IconPlus size={14} />
                Agregar ítem
              </button>
            }
          </div>
        </div>
      </div>
    </div>);

}

Object.assign(window, { ItemsTable, InventoryCombobox });
