// ─── Inventario · Detalle ────────────────────────────────────────────────
// InventarioDetailScreen: alta/edición/lectura in-place de un producto (grid
// de campos + selector de proveedor) + dos sub-tablas de solo lectura (Lotes,
// Historial de movimientos). Extraído (P10) de screens-inventario.jsx junto
// con lista.jsx (InventarioScreen) y historial.jsx; los helpers de vencimiento
// (invLoteEstado/INV_LOTE_CFG/invTiempoRestante/INV_ESTADO_CFG/INV_CATEGORIAS)
// viven en compartido.jsx. Expone a window: InventarioDetailScreen. Consume
// window.INVENTARIO (core/colecciones.js), MicosStore.create/update/remove
// (core/store.js), useDocEdit/buildDocActions/DocCard/ConfirmDeleteModal (ui/
// doc-kit-*.jsx), MicosMetaCell (ui/doc-kit-meta.jsx), PartySelectorModal (ui/
// party-selector.jsx), ExportModal (ui/entity-list.jsx, fix auditoría
// 05/07/2026 — antes el botón "Exportar" no tenía wiring), TablePill (ui/
// primitives.jsx), formatMoney/formatDate (dominio/formato.js),
// INVENTARIO_ESTADO (core/bundle-config.js), invLoteEstado/INV_LOTE_CFG/
// invTiempoRestante/INV_ESTADO_CFG/INV_CATEGORIAS (compartido.jsx).
//
// FIX (mapa-comprension.md §inv-dash, severidad media — "makeBlankInventario
// fabrica un id cliente"): antes `makeBlankInventario()` fabricaba `id`
// ('inv-'+Date.now()) y `publicId` (MicosNextSeqId local) y, tras
// `MicosStore.create()`, `Object.assign(created, obj)` PISABA el id temporal
// `_tmp-inv-…` que el store le asigna a ESE MISMO objeto — cuando el POST
// resolvía, el store volvía a mutar el objeto con el id/publicId REALES del
// server, pero `selectedInventarioId` (estado de React, sembrado con el id
// fabricado) nunca se enteraba → el detalle dejaba de encontrar el producto
// por id y caía al fallback `INVENTARIO[0]` (mostraba OTRO producto sin
// aviso). Mismo patrón ya identificado y eliminado para Expedientes
// (BUGS-caza-01-07-2026.md #25/B1): sin id/publicId fabricado, el trigger de
// la DB asigna el `public_id` real al guardar (`trg_public_id_inventario`,
// verificado `\d inventario`) — el header muestra el SKU tipeado como "big id"
// mientras no hay publicId todavía (no hace falta un id visible pre-save).
//
// FIX (mapa-comprension.md §inv-dash, severidad media — "guard de
// fetch-and-merge"): `INVENTARIO.find(...) || INVENTARIO[0]` mostraba
// SILENCIOSAMENTE otro producto si el id buscado no estaba en la ventana
// hidratada al boot (catálogo > 1000 filas — PostgREST cappea sin ?limit — o
// un producto cargado por el scroll infinito de InventarioScreen que quedó
// fuera de esa ventana). Mismo criterio que `openExpedienteFor`/`applyPath`
// (core/router.jsx): si no está en memoria, se trae por id (GET
// /api/inventario/<id>, sirve tanto uuid como publicId — `idColumn()`,
// src/lib/api/rest.ts) y se mergea en `window.INVENTARIO`; si tampoco existe
// en el server, pantalla de "no encontrado" honesta — NUNCA cae a
// INVENTARIO[0].
//
// P24 (03/07/2026 — libro de movimientos) + fixer post-review (hallazgo MEDIA #1, 03/07/2026):
// "Stock actual" cambia `inventario.stock` — si el guardado cambia el stock, un modal pide el
// MOTIVO real (no se inventa uno) y ese motivo viaja como HINT en el mismo PATCH (4to argumento
// `meta` de `MicosStore.update`, ver core/store.js) — el SERVER (`PATCH /api/inventario/[id]`)
// escribe la fila de `inventario_movimiento` (tipo AJUSTE + el delta con signo) en el mismo
// request que mueve el stock; ya no un POST aparte del browser que podía fallar sin revertirlo.
// Ver el modal al final del archivo y el `onSave` async de useDocEdit (puede pausar/abortar el
// guardado devolviendo una Promise/`false`, mismo mecanismo que ya usa Logística para el FK de
// expediente).
//
// EXCEPCIÓN DE TECHO (sobre el techo duro ~400, ver tabla de ESTRUCTURA.md
// para el número real — FIX P13 ítem 16,
// Review P10 BAJA-5: faltaba esta nota; P24 lo sube más con el modal de motivo de
// arriba): un solo tema (alta/edición/lectura de UN producto) con 4 sub-bloques propios del
// dominio (grid de campos + selector de proveedor + 2 sub-tablas de solo lectura
// Lotes/Historial + el modal de motivo del ajuste de stock) más el guard de fetch-and-merge
// de arriba — partirlo más metería el mismo `item`/`docEdit` compartido entre archivos sin
// ganar cohesión real.

const { useState: _useStateInvD, useMemo: _useMemoInvD, useEffect: _useEffectInvD } = React;

function makeBlankInventario() {
  return {
    // Sin id/publicId fabricado (ver header): store.create() asigna el id
    // temporal `_tmp-…`; el public_id real lo asigna el trigger de la DB.
    sku: '', descripcion: '', proveedor: '', proveedorId: null,
    categoria: '', ubicacion: '', costoUnitario: 0, stock: 0, minimo: 0,
    lotes: [], historial: [],
  };
}

function InventarioDetailScreen({ inventarioId, isCreate = false, startEditing = false, onBack, onSaved, onDeleted, onVerHistorial }) {
  const blank = _useMemoInvD(() => (isCreate ? makeBlankInventario() : null), [isCreate]);

  // Guard fetch-and-merge (ver header): si el producto no está en la ventana
  // hidratada lo trae por id y lo mergea en window.INVENTARIO antes de
  // renderizar — nunca cae a INVENTARIO[0].
  const foundLocal = isCreate ? null : (window.INVENTARIO || []).find((i) => i.id === inventarioId);
  const [mergeTick, setMergeTick] = _useStateInvD(0);
  const [notFound, setNotFound] = _useStateInvD(false);
  _useEffectInvD(() => {
    if (isCreate || foundLocal || !inventarioId) return undefined;
    let cancelled = false;
    fetch(`/api/inventario/${encodeURIComponent(inventarioId)}`, { credentials: 'same-origin' })
      .then((r) => (r.ok ? r.json() : Promise.reject(r.status)))
      .then((server) => {
        if (cancelled || !server || !server.id) throw new Error('sin id');
        const col = window.INVENTARIO || (window.INVENTARIO = []);
        if (!col.some((x) => x.id === server.id)) col.push(server);
        setMergeTick((t) => t + 1);
      })
      .catch((err) => {
        if (cancelled) return;
        console.warn('[InventarioDetailScreen] fetch-and-merge', inventarioId, err);
        setNotFound(true);
      });
    return () => { cancelled = true; };
  }, [isCreate, inventarioId, foundLocal]);

  const item = isCreate ? blank : foundLocal;

  const [verTodo, setVerTodo] = _useStateInvD(false);
  const [toast, setToast] = _useStateInvD(null);
  const [provPicker, setProvPicker] = _useStateInvD(false);
  const [confirmDel, setConfirmDel] = _useStateInvD(false);
  // Hallazgo auditoría 05/07/2026 ("Exportar" del detalle sin onClick): a diferencia de
  // lista.jsx (que usa EntityListScreen/ExportModal para el catálogo completo), esta
  // pantalla no tenía NINGÚN wiring de export — ni estado ni modal. Reusa el MISMO
  // ExportModal (ui/entity-list.jsx) con un exportConfig de un solo registro (este ítem),
  // igual criterio que invExportConfig de lista.jsx.
  const [exportOpen, setExportOpen] = _useStateInvD(false);

  // P24 (03/07/2026 — libro de movimientos): "Stock actual" es un campo editable a mano acá
  // (ver infoFields más abajo) — antes cambiaba `inventario.stock` (PATCH real) sin dejar
  // NINGÚN rastro. Ahora, si el guardado cambia el stock, se pide un MOTIVO real (no se
  // inventa un texto genérico — a diferencia de logística/ajustes, acá no hay un documento
  // origen que ya explique el porqué) antes de confirmar: el modal PAUSA el guardado
  // (onSave async devuelve una Promise que se resuelve al confirmar/cancelar el modal, mismo
  // mecanismo de "onSave puede abortar con `return false`" que ya usa Logística para el FK de
  // expediente — ver ui/doc-kit-edit.jsx). Cancelar el modal aborta TODO el guardado (sigue
  // editando, nada se persiste) — no hay "guardar sin motivo".
  const [stockAdjustOpen, setStockAdjustOpen] = _useStateInvD(false);
  const [stockAdjustDelta, setStockAdjustDelta] = _useStateInvD(0);
  const [stockAdjustMotivo, setStockAdjustMotivo] = _useStateInvD('');
  const stockAdjustResolveRef = React.useRef(null);
  const confirmStockAdjust = () => {
    const m = stockAdjustMotivo.trim();
    if (!m || !stockAdjustResolveRef.current) return;
    setStockAdjustOpen(false);
    stockAdjustResolveRef.current(m);
  };
  const cancelStockAdjust = () => {
    setStockAdjustOpen(false);
    if (stockAdjustResolveRef.current) stockAdjustResolveRef.current(null);
  };

  // useDocEdit SIEMPRE se llama (nunca condicionalmente, para no saltear
  // hooks entre renders) — con `item` todavía sin resolver, arranca con un
  // draft vacío; en cuanto `foundLocal`/`mergeTick` resuelve, useDocEdit vuelve
  // a sembrar (su `initialFn` corre de nuevo si cambia `item`, ver ui/doc-kit-edit.jsx).
  const docEdit = window.useDocEdit(
    () => ({
      sku: item?.sku || '', descripcion: item?.descripcion || '',
      proveedor: item?.proveedor || '', proveedorId: item?.proveedorId || null,
      categoria: item?.categoria || '', ubicacion: item?.ubicacion || '',
      // P24: '' (no 0) cuando no hay costo real cargado — el input arranca VACÍO, no
      // insinuando un cero real (ver el mismo criterio en onSave, más abajo).
      costoUnitario: item?.costoUnitario ?? '', stock: item?.stock || 0, minimo: item?.minimo || 0,
    }),
    {
      startEditing: isCreate || startEditing,
      onCancel: () => { if (isCreate || startEditing) onBack && onBack(); },
      onSave: async (d) => {
        // P24: '' (input vacío, sin tocar) o null preserva "sin dato" — SOLO un número
        // realmente tipeado (incl. "0" explícito) se guarda como costo real. Antes
        // `Number(d.costoUnitario) || 0` convertía CUALQUIER guardado (aunque el usuario solo
        // haya tocado stock/ubicación/etc.) en un costo_unitario=0 real en la DB — un producto
        // sin costo cargado (NULL, honesto) pasaba a "cuesta $0" (mentira) con solo re-guardar.
        const costoUnitarioVal = (d.costoUnitario === '' || d.costoUnitario == null) ? null : (Number(d.costoUnitario) || 0);
        const obj = {
          ...item,
          sku: (d.sku || '').trim(), descripcion: (d.descripcion || '').trim(),
          proveedor: (d.proveedor || '').trim(), proveedorId: d.proveedorId || null,
          categoria: d.categoria || '', ubicacion: (d.ubicacion || '').trim(),
          costoUnitario: costoUnitarioVal, stock: Number(d.stock) || 0, minimo: Number(d.minimo) || 0,
        };
        obj.valuacion = costoUnitarioVal != null ? obj.stock * costoUnitarioVal : null;
        obj.lotes = item.lotes || [];
        obj.historial = item.historial || [];
        // Columnas reales de `inventario` (schema real, docker exec \d inventario): sku,
        // descripcion, categoria, subcategoria, stock, minimo, ubicacion, costo_unitario,
        // proveedor_id (P24, antes NO existía — ver migración 20260703140000).
        // valuacion/lotes/historial/proveedor (el NOMBRE resuelto, no el id) NO son columnas
        // (derivados o tablas hijas) — mandarlas hacía que el backend rechazara TODO el patch
        // con 400 (bug A3) y el store aplicaba el optimista igual, perdiendo hasta sku/stock
        // en el camino. El backend ya filtra esto de raíz (whitelist en rest.ts), pero acá
        // además no mandamos basura de entrada: solo el payload con columnas reales viaja
        // a la red; los campos derivados/locales se mantienen en memoria de sesión.
        const payload = {
          sku: obj.sku, descripcion: obj.descripcion, categoria: obj.categoria,
          subcategoria: obj.subcategoria, ubicacion: obj.ubicacion,
          costoUnitario: obj.costoUnitario, stock: obj.stock, minimo: obj.minimo,
          // P24: proveedorId ya es columna real (migración 20260703140000) — antes se
          // descartaba en silencio acá mismo (el whitelist del backend lo rechazaba igual,
          // pero esto documentaba "no mandamos basura de entrada" con un campo que YA no es
          // basura). Ver src/lib/api/serializers/inventario.ts.
          proveedorId: obj.proveedorId,
        };

        // Pide motivo SOLO si cambia el stock de un producto YA existente (una alta no
        // tiene "delta" — es el stock inicial, no un ajuste).
        const stockDelta = isCreate ? 0 : (obj.stock - (Number(item.stock) || 0));
        let motivo = null;
        if (stockDelta !== 0) {
          setStockAdjustDelta(stockDelta);
          setStockAdjustMotivo('');
          setStockAdjustOpen(true);
          motivo = await new Promise((resolve) => { stockAdjustResolveRef.current = resolve; });
          if (motivo == null) return false; // canceló el modal: aborta TODO el guardado
        }

        if (isCreate) {
          // Sin id/publicId fabricado (ver header) — store.create() asigna el
          // `_tmp-…`, `onSaved` viaja con ESE id (el mismo que usará
          // `selectedInventarioId` para el próximo find, sin desincronizarse).
          const created = window.MicosStore.create('inventario', { ...payload, __nuevo: true });
          Object.assign(created, obj); // obj ya no trae id/publicId (blank sin fabricar) — no pisa el _tmp- del store
          onSaved && onSaved(created.id);
        } else {
          // Fixer post-review (hallazgo MEDIA #1, 03/07/2026): el hint de ledger (tipo AJUSTE +
          // el motivo real tipeado en el modal) viaja en el MISMO PATCH — 4to argumento `meta`
          // (ver core/store.js) — así el SERVER (`PATCH /api/inventario/[id]`) escribe la fila
          // en el mismo request que mueve el stock, ya no un POST aparte que podía fallar sin
          // revertir el stock.
          window.MicosStore.update('inventario', item.id, payload, stockDelta !== 0 ? {
            movimientoTipo: 'AJUSTE', movimientoReferencia: item.publicId || null,
            movimientoMotivo: motivo, movimientoFecha: window.MicosTodayISO ? window.MicosTodayISO() : undefined,
          } : undefined);
          Object.assign(item, obj); // ídem: la UI sigue viendo los derivados aunque no viajen
          setToast(`Producto ${obj.sku || obj.publicId} guardado`);
          setTimeout(() => setToast(null), 3600);
        }
      },
    }
  );
  const { editing, draft, setDraft, enterEdit, discardEdit, saveEdit, patch } = docEdit;

  // Todavía resolviendo (ni encontrado local ni fetch-and-merge terminado) —
  // no renderizar nada con `item` undefined. `notFound` cubre el caso real de
  // "no existe" (server 404): mensaje honesto, nunca un producto ajeno.
  if (!isCreate && !item) {
    if (notFound) {
      return (
        <div className="space-y-6 pt-2">
          <button onClick={onBack} className="flex items-center gap-2 text-[11px] font-bold uppercase tracking-[0.12em] cursor-pointer" style={{ color: 'var(--muted-foreground)' }}>
            <IconArrowLeft size={13} />Volver a Inventario
          </button>
          <p className="text-[14px] font-semibold" style={{ color: 'var(--muted-foreground)' }}>No se encontró el producto.</p>
        </div>
      );
    }
    return null;
  }

  const liveItem = editing && draft ? { stock: Number(draft.stock) || 0, minimo: Number(draft.minimo) || 0 } : item;
  const est    = INVENTARIO_ESTADO(liveItem);
  const estCfg  = INV_ESTADO_CFG[est];
  const Cell    = window.MicosMetaCell;

  const skuShown  = editing && draft ? draft.sku : item.sku;
  const descShown = editing && draft ? draft.descripcion : item.descripcion;
  const provShown = editing && draft ? draft.proveedor : item.proveedor;
  // Sin publicId todavía (alta, antes de guardar): mostrar el SKU tipeado o
  // "Nuevo producto" en vez de `undefined` (patrón "Borrador" de Expedientes).
  const bigId = editing && draft
    ? (draft.sku || item.publicId || (isCreate ? 'Nuevo producto' : ''))
    : (item.publicId || item.sku);

  const actions = editing
    ? window.buildDocActions({ editing, enterEdit, discardEdit, saveEdit, onDelete: (!isCreate) ? () => setConfirmDel(true) : undefined })
    : (
      <>
        <button className="btn btn-outline" onClick={() => setExportOpen(true)}><IconDownload size={15} /><span className="hidden sm:inline">Exportar</span></button>
        <button className="btn btn-primary" onClick={enterEdit}><IconPencil size={14} /><span className="hidden sm:inline">Editar</span></button>
      </>
    );

  const exportConfig = [
    { label: 'ID', value: (i) => i.publicId },
    { label: 'SKU', value: (i) => i.sku },
    { label: 'Descripción', value: (i) => i.descripcion },
    { label: 'Categoría', value: (i) => i.categoria },
    { label: 'Proveedor', value: (i) => i.proveedor },
    { label: 'Ubicación', value: (i) => i.ubicacion },
    { label: 'Stock', value: (i) => i.stock },
    { label: 'Stock mínimo', value: (i) => i.minimo },
    { label: 'Costo unitario', value: (i) => i.costoUnitario },
    { label: 'Valuación', value: (i) => i.valuacion },
  ];

  // Hallazgo auditoría 05/07/2026 ("Agregar sección" no crea nada real): el botón
  // "Agregar sección" de lista.jsx guarda la sección nueva en localStorage
  // ('micos-inv-categorias') pero este select (el ÚNICO lugar que asigna `categoria` a
  // un producto — columna real de `inventario`, texto libre) solo ofrecía INV_CATEGORIAS
  // — la sección creada quedaba huérfana, nunca asignable a ningún producto. Se lee la
  // MISMA clave acá para que la sección recién creada aparezca como opción real: una vez
  // elegida y guardada, es una escritura real a `inventario.categoria` (no localStorage).
  const customCatsFromStorage = (() => {
    try { return JSON.parse(localStorage.getItem('micos-inv-categorias') || '[]'); } catch (e) { return []; }
  })();
  const categoriaOptions = [
    { value: '', label: 'Sin categoría' },
    ...INV_CATEGORIAS,
    ...customCatsFromStorage.filter((c) => c && !INV_CATEGORIAS.includes(c)),
  ];

  const infoFields = [
    { label: 'Código',         editable: editing, input: 'text', mono: true, accent: true,
      value: editing ? draft.sku : item.sku, onChange: (v) => patch('sku', v), placeholder: 'Ej: BOL-SRF-500' },
    { label: 'Descripción',    editable: editing, input: 'text',
      value: editing ? draft.descripcion : item.descripcion, onChange: (v) => patch('descripcion', v), placeholder: 'Descripción del producto' },
    { label: 'Categoría',      editable: editing, input: 'select',
      options: categoriaOptions,
      value: editing ? draft.categoria : (item.categoria || '—'), onChange: (v) => patch('categoria', v) },
    { label: 'Ubicación',      editable: editing, input: 'text',
      value: editing ? draft.ubicacion : (item.ubicacion || '—'), onChange: (v) => patch('ubicacion', v), placeholder: 'Ej: Depósito B · 5-01' },
    { label: 'Costo unitario', editable: editing, input: 'number', mono: true,
      value: editing ? draft.costoUnitario : formatMoney(item.costoUnitario), onChange: (v) => patch('costoUnitario', v) },
    { label: 'Stock actual',   editable: editing, input: 'number', mono: true,
      value: editing ? draft.stock : String(item.stock), onChange: (v) => patch('stock', v) },
    { label: 'Stock mínimo',   editable: editing, input: 'number', mono: true,
      value: editing ? draft.minimo : String(item.minimo), onChange: (v) => patch('minimo', v) },
  ];

  const historialRows = verTodo ? item.historial : item.historial.slice(0, 5);

  // ── sub-table shell ──────────────────────────────
  function SubTableHead({ cols }) {
    return (
      <div className="grid gap-3 px-4 py-3 text-[10.5px] font-bold uppercase tracking-[0.08em]"
        style={{ gridTemplateColumns: cols.map(c => c.w).join(' '), color: 'var(--muted-foreground)', borderBottom: '1px solid color-mix(in oklab, var(--border) 55%, transparent)', background: 'color-mix(in oklab, var(--surface) 50%, transparent)' }}>
        {cols.map(c => <div key={c.label} className={c.right ? 'text-right' : c.center ? 'text-center' : ''}>{c.label}</div>)}
      </div>
    );
  }

  return (
    <div className="space-y-7 pt-2 animate-fade-in-up">

      {/* ── Back link ───────────────────────────── */}
      <button onClick={onBack}
        className="back-link flex items-center gap-2 text-[11px] font-bold uppercase tracking-[0.12em] cursor-pointer transition-colors"
        style={{ color: 'var(--muted-foreground)' }}
        onMouseEnter={e => e.currentTarget.style.color = 'var(--accent)'}
        onMouseLeave={e => e.currentTarget.style.color = 'var(--muted-foreground)'}>
        <IconArrowLeft size={13} />
        Volver a Inventario
      </button>

      {/* ── Header ──────────────────────────────── */}
      <div className="flex flex-wrap items-start justify-between gap-4" style={{ animationDelay: '40ms' }}>
        <div className="space-y-3 min-w-0">
          <div className="flex items-center gap-3 flex-wrap">
            <span className="text-[11px] font-bold uppercase tracking-[0.18em]" style={{ color: 'var(--accent)' }}>
              Inventario · Ítem
            </span>
            <TablePill label={estCfg.label} tone={estCfg.tone} />
            {editing && (
              <span className="pill pill-presupuesto" style={{ padding: '4px 12px', fontSize: '10.5px' }}>
                {isCreate ? 'Nuevo' : 'Editando'}
              </span>
            )}
          </div>
          <h1 className="font-mono text-[clamp(28px,4.2vw,40px)] leading-[1.05] font-extrabold tracking-[-0.01em] break-all"
            style={{ color: 'var(--foreground)' }}>{bigId}</h1>
          <p className="text-[13.5px] font-medium" style={{ color: 'var(--muted-foreground)' }}>
            {descShown || 'Sin descripción'}
            <span className="mx-1.5 opacity-50">·</span>
            <span className="font-bold" style={{ color: 'var(--foreground)' }}>{provShown || 'Sin proveedor'}</span>
          </p>
        </div>
        <div className="flex items-center gap-2 flex-shrink-0 flex-wrap">
          {actions}
        </div>
      </div>

      {/* ── Información general ──────────────────── */}
      <DocCard className="p-7 sm:p-8" editing={editing}>
        <p className="text-[11px] font-bold uppercase tracking-[0.2em] mb-6" style={{ color: 'var(--accent)' }}>
          Información general
        </p>
        <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-x-8 gap-y-6">
          {infoFields.map((f, i) => <Cell key={i} it={f} />)}
          {/* Proveedor (selector del directorio) */}
          <div className="space-y-2 min-w-0">
            <p className="text-[10.5px] font-bold uppercase tracking-[0.14em]" style={{ color: 'var(--muted-foreground)' }}>Proveedor</p>
            {editing ? (
              <button type="button" onClick={() => setProvPicker(true)}
                className="edit-input w-full text-left text-[17px] font-extrabold leading-none tracking-tight"
                style={{ color: provShown ? 'var(--foreground)' : 'color-mix(in oklab, var(--muted-foreground) 80%, transparent)' }}>
                {provShown || 'Elegir proveedor…'}
              </button>
            ) : (
              <p className="text-[16px] font-extrabold leading-tight" style={{ color: 'var(--foreground)' }}>{item.proveedor || '—'}</p>
            )}
          </div>
          {/* Estado (derivado) */}
          <div className="space-y-2">
            <p className="text-[10.5px] font-bold uppercase tracking-[0.14em]" style={{ color: 'var(--muted-foreground)' }}>Estado</p>
            <TablePill label={estCfg.label} tone={estCfg.tone} />
          </div>
        </div>
      </DocCard>

      {/* ── Lotes y vencimientos ─────────────────── */}
      <DocCard className="p-7 sm:p-8">
        <div className="flex items-center justify-between mb-6">
          <p className="text-[11px] font-bold uppercase tracking-[0.2em]" style={{ color: 'var(--accent)' }}>
            Lotes y vencimientos
          </p>
          {/* Fixer post-review (hallazgo LOW #2, paquetes/review-inventario.md): no-op honesto —
              no hay ruta de escritura de `inventario_lote` todavía (deuda documentada en §6 del
              reporte P24), así que el botón se deshabilita en vez de fingir que hace algo al
              clickear. `disabled` dispara el `.btn:disabled` global (gris plano, sin hover/lift,
              cursor not-allowed — CLAUDE.md §2/§3), el `title` explica el porqué. */}
          <button disabled title="Todavía no se puede cargar lotes desde acá — no implementado"
            className="btn btn-outline" style={{ height: 32, fontSize: 12, padding: '0 12px' }}>
            <IconPlus size={12} /> Agregar lote
          </button>
        </div>
        <div className="rounded-xl overflow-hidden" style={{ border: '1px solid color-mix(in oklab, var(--border) 55%, transparent)' }}>
          <SubTableHead cols={[
            { label: 'Lote',        w: '170px' },
            { label: 'Vencimiento', w: '120px' },
            { label: 'T. restante', w: '140px' },
            { label: 'Cant.',       w: '70px',  right: true },
            { label: 'Ubicación',   w: '1fr'   },
            { label: 'Estado',      w: '120px', center: true },
            { label: '',            w: '76px'  },
          ]} />
          {item.lotes.map((lote, idx) => {
            const lEst   = invLoteEstado(lote.vencimiento);
            const lCfg   = INV_LOTE_CFG[lEst];
            const tr     = invTiempoRestante(lote.vencimiento);
            const trColor = tr.tone === 'neg' ? 'var(--danger)' : tr.tone === 'warn' ? 'var(--warning)' : 'var(--muted-foreground)';
            const isLast = idx === item.lotes.length - 1;
            return (
              <div key={lote.id} className="grid gap-3 px-4 py-3.5 items-center"
                style={{ gridTemplateColumns: '170px 120px 140px 70px 1fr 120px 76px', borderBottom: !isLast ? '1px solid color-mix(in oklab, var(--border) 45%, transparent)' : 'none' }}>
                <span className="font-mono text-[11px] font-bold tracking-wide uppercase truncate" style={{ color: 'var(--accent)' }}>{lote.lote}</span>
                <span className="text-[12px] font-mono tabular-nums" style={{ color: 'var(--foreground)' }}>{formatDate(lote.vencimiento)}</span>
                <span className="text-[12px] font-bold" style={{ color: trColor }}>{tr.text}</span>
                <span className="text-[14px] font-extrabold tabular-nums text-right" style={{ color: 'var(--foreground)' }}>{lote.cantidad}</span>
                <span className="text-[12px] truncate" style={{ color: 'var(--muted-foreground)' }}>{lote.ubicacion}</span>
                <div className="flex justify-center"><TablePill label={lCfg.label} tone={lCfg.tone} /></div>
                {/* Fixer post-review (hallazgo LOW #2): mismo criterio que "Agregar lote" arriba —
                    sin ruta de escritura de `inventario_lote`, así que quedan deshabilitados
                    (gris plano fijo, sin hover que insinúe que reaccionan) en vez de activos
                    sin `onClick`. */}
                <div className="flex items-center justify-end gap-1.5">
                  <button disabled title="Editar lote — todavía no implementado"
                    className="inline-flex items-center justify-center rounded-lg"
                    style={{ width: 28, height: 28, color: 'var(--muted-foreground)', background: 'color-mix(in oklab, var(--muted-foreground) 8%, transparent)', cursor: 'not-allowed', opacity: 0.55 }}>
                    <IconPencil size={12} />
                  </button>
                  <button disabled title="Eliminar lote — todavía no implementado"
                    className="inline-flex items-center justify-center rounded-lg"
                    style={{ width: 28, height: 28, color: 'var(--muted-foreground)', background: 'color-mix(in oklab, var(--muted-foreground) 8%, transparent)', cursor: 'not-allowed', opacity: 0.55 }}>
                    <IconTrash size={12} />
                  </button>
                </div>
              </div>
            );
          })}
          {item.lotes.length === 0 && (
            <div className="px-4 py-10 text-center">
              <p className="text-[13px] font-semibold" style={{ color: 'var(--muted-foreground)' }}>Sin lotes cargados.</p>
            </div>
          )}
        </div>
      </DocCard>

      {/* ── Historial de movimientos ─────────────── */}
      <DocCard className="p-7 sm:p-8">
        <div className="flex items-center justify-between mb-6">
          <p className="text-[11px] font-bold uppercase tracking-[0.2em]" style={{ color: 'var(--accent)' }}>
            Historial de movimientos
          </p>
          <div className="flex items-center gap-4">
            {item.historial.length > 5 && (
              <button onClick={() => setVerTodo(v => !v)}
                className="text-[11.5px] font-bold uppercase tracking-[0.1em] flex items-center gap-1"
                style={{ color: 'var(--accent)' }}>
                {verTodo ? 'Mostrar menos' : 'Ver todos'}
                <IconArrowUpRight size={12} />
              </button>
            )}
            {!isCreate && onVerHistorial && (
              <button onClick={onVerHistorial} className="btn btn-outline" style={{ height: 32, fontSize: 12, padding: '0 12px' }}>
                <IconClock size={12} /> Ver todo el historial
              </button>
            )}
          </div>
        </div>
        <div className="rounded-xl overflow-hidden" style={{ border: '1px solid color-mix(in oklab, var(--border) 55%, transparent)' }}>
          <SubTableHead cols={[
            { label: 'Fecha',       w: '110px' },
            { label: 'Tipo',        w: '100px' },
            { label: 'Cant.',       w: '80px',  right: true },
            { label: 'Lote',        w: '170px' },
            { label: 'Referencia',  w: '1fr'   },
            { label: 'Usuario',     w: '120px' },
          ]} />
          {historialRows.map((mov, idx) => {
            const isIngreso = mov.tipo === 'INGRESO';
            const cantColor = isIngreso ? 'var(--success)' : 'var(--danger)';
            const isLast    = idx === historialRows.length - 1;
            return (
              <div key={mov.id} className="grid gap-3 px-4 py-3.5 items-center"
                style={{ gridTemplateColumns: '110px 100px 80px 170px 1fr 120px', borderBottom: !isLast ? '1px solid color-mix(in oklab, var(--border) 45%, transparent)' : 'none' }}>
                <span className="text-[12px] font-mono tabular-nums" style={{ color: 'var(--muted-foreground)' }}>{formatDate(mov.fecha)}</span>
                <TablePill label={isIngreso ? 'Ingreso' : 'Egreso'} tone={isIngreso ? 'pos' : 'neg'} />
                <span className="text-[14px] font-extrabold tabular-nums text-right" style={{ color: cantColor }}>
                  {isIngreso ? '+' : '−'}{mov.cantidad}
                </span>
                <span className="font-mono text-[11px] font-bold tracking-wide uppercase truncate" style={{ color: 'var(--muted-foreground)' }}>{mov.lote}</span>
                {/* Hallazgo auditoría 05/07/2026 ("Referencia" con cara de link, no navega):
                    era un <button> con afordance completa de link (color accent + underline
                    en hover) sin onClick — no hay ruta de navegación real (referencia es texto
                    libre del ledger, no una FK a un documento) ni prop para cablearla desde
                    afuera (ver app.jsx). Mismo criterio que historial.jsx (el ledger completo,
                    misma columna): texto plano, sin afordance de clickeable. */}
                <span className="font-mono text-[11.5px] font-semibold tracking-wide truncate block" style={{ color: 'var(--foreground)' }}>
                  {mov.referencia || '—'}
                </span>
                <span className="text-[12.5px] font-semibold truncate" style={{ color: 'var(--foreground)' }}>{mov.usuario || 'Sin usuario'}</span>
              </div>
            );
          })}
          {item.historial.length === 0 && (
            <div className="px-4 py-10 text-center">
              <p className="text-[13px] font-semibold" style={{ color: 'var(--muted-foreground)' }}>{HISTORIAL_EMPTY_TEXT}</p>
            </div>
          )}
        </div>
      </DocCard>

      {/* Toast de confirmación */}
      {toast && (
        <div className="fixed bottom-6 right-6 z-50 flex items-center gap-3 rounded-xl px-4 py-3 animate-fade-in-up"
          style={{ background: 'var(--card)', border: '1px solid color-mix(in oklab, var(--accent) 45%, transparent)', boxShadow: '0 18px 50px -14px rgba(8, 20, 24, 0.5)', maxWidth: 'min(92vw, 380px)' }}>
          <span className="inline-flex items-center justify-center rounded-lg flex-shrink-0"
            style={{ width: 30, height: 30, background: 'color-mix(in oklab, var(--accent) 16%, transparent)', color: 'var(--accent)' }}>
            <IconCheck size={16} />
          </span>
          <p className="text-[12.5px] font-bold truncate" style={{ color: 'var(--foreground)' }}>{toast}</p>
        </div>
      )}

      <ExportModal
        open={exportOpen}
        onClose={() => setExportOpen(false)}
        exportConfig={exportConfig}
        rows={item ? [item] : []}
        filename={(item && (item.sku || item.publicId)) || 'producto'}
      />

      {provPicker && window.PartySelectorModal && (
        <window.PartySelectorModal
          type="proveedor"
          open={true}
          onClose={() => setProvPicker(false)}
          onPick={(obj) => {
            if (obj && obj.id) setDraft((d) => ({ ...d, proveedor: obj.razonSocial || obj.nombre || '', proveedorId: obj.id }));
            setProvPicker(false);
          }}
        />
      )}
      {confirmDel && window.ConfirmDeleteModal && (
        <window.ConfirmDeleteModal
          title="¿Eliminar producto?"
          itemName={item.sku || item.publicId}
          description="Se moverá al archivo. Esta acción se puede revertir desde el visor de datos."
          onCancel={() => setConfirmDel(false)}
          onConfirm={() => {
            window.MicosStore.remove('inventario', item.id);
            setConfirmDel(false);
            onDeleted && onDeleted();
          }}
        />
      )}

      {/* Modal: motivo del ajuste manual de stock (P24 — mismo patrón que "Nueva sección"
          de lista.jsx). Pausa el guardado hasta que el usuario explique el porqué; cancelar
          aborta TODO el guardado (ver onSave más arriba). */}
      {stockAdjustOpen && (
        <div className="fixed inset-0 z-50 flex items-center justify-center p-4" style={{ background: 'rgba(8,20,24,0.55)' }} onClick={cancelStockAdjust}>
          <div className="rounded-2xl w-full max-w-sm animate-fade-in-up" onClick={(e) => e.stopPropagation()}
            style={{ background: 'var(--card)', border: '1px solid var(--border)', boxShadow: '0 24px 60px -16px rgba(8,20,24,0.6)' }}>
            <div className="px-6 pt-6 pb-1">
              <p className="text-[15px] font-extrabold" style={{ color: 'var(--foreground)' }}>Motivo del ajuste de stock</p>
              <p className="text-[12.5px] mt-1" style={{ color: 'var(--muted-foreground)' }}>
                Estás {stockAdjustDelta > 0 ? 'sumando' : 'restando'} {Math.abs(stockAdjustDelta)} unidad{Math.abs(stockAdjustDelta) === 1 ? '' : 'es'} a mano — queda en el historial con este motivo.
              </p>
            </div>
            <div className="px-6 py-4">
              <label className="text-[10.5px] font-bold uppercase tracking-[0.14em]" style={{ color: 'var(--muted-foreground)' }}>Motivo</label>
              <input autoFocus value={stockAdjustMotivo}
                onChange={(e) => setStockAdjustMotivo(e.target.value)}
                onKeyDown={(e) => { if (e.key === 'Enter' && stockAdjustMotivo.trim()) confirmStockAdjust(); if (e.key === 'Escape') cancelStockAdjust(); }}
                placeholder="Ej: Conteo físico, rotura, corrección de carga…"
                className="edit-input w-full mt-2 text-[14px] font-bold" style={{ color: 'var(--foreground)' }} />
            </div>
            <div className="px-6 pb-6 pt-1 flex items-center justify-end gap-2">
              <button onClick={cancelStockAdjust} className="btn btn-outline">Cancelar</button>
              <button onClick={confirmStockAdjust} disabled={!stockAdjustMotivo.trim()} className="btn btn-primary">Confirmar ajuste</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

Object.assign(window, { InventarioDetailScreen });
