// ─────────────────────────────────────────────────────
// GUT-272 (Juanjo 8 oct) — Una sola tabla de materiales para Expediente › Materiales
// (bom.jsx) y Programación › Materiales (programacion.jsx: drawer del proyecto y
// listado global). Las dos leen `bom_lineas`; las columnas extra (medidas, stock,
// enviadas) salen de src/services/materialesTabla.js en las dos APIs.
//
//   Columnas: ID · Descripción (+proveedor) · Largo · Ancho · Alto · Cant. · Specs ·
//             Stock · Enviadas · Estado · Observación · acciones
//   - Las medidas, la cantidad, la descripción, el proveedor… solo se cambian con
//     «Editar» (fila de edición), para no tocarlos sin querer. La observación sí se
//     escribe en la propia celda (es una nota).
//   - Sin columna de fechas: pedido / estimada / recepción salen en la fila de
//     edición y al pasar el ratón por el estado.
//   - Stock = saldo del artículo del catálogo (Almacén); «—» si es texto libre.
//   - Enviadas = uds de los albaranes de entrega vigentes / cantidad, y «A enviar» (INC-013).
//
// Props:
//   grupos       [{ key, titulo?, lineas }] (titulo → fila separadora, p. ej. PERFILES)
//   guardar      (linea, body) => Promise. body con nombres de bom_lineas: nombre, codigo,
//                largo, ancho, alto, cantidad, unidad, ral, apertura, guia, proveedor_id,
//                elemento_id, observacion, material_id. Cada pantalla lo manda a su API.
//   borrar       (linea) => Promise
//   elementos    cortinas del expediente (si viene, la edición deja reasignar cortina)
//   seleccion    { sel: Set, marcar(id, on) } → casilla «Crear pedido» (Expediente)
//   extraCols    [{ key, th, thProps?, td(linea) }] columnas delante (Proyecto, Instalación…)
//   mostrarCortina  pinta la cortina junto a la descripción (cuando no se agrupa por ella)
//   rowClass     (linea) => className
//
// INC-009 (Juanjo): las líneas de un mismo despliegue de kit (kit_grupo) van juntas
// bajo una fila «KIT …» plegable (plegada por defecto; se recuerda en este navegador)
// con nº de componentes, nº de kits y estado agregado (pedido / recibido / enviado
// x de y). La casilla de la cabecera marca o desmarca los componentes que se pueden
// pedir. Una columna extra puede pintar su celda en la cabecera con tdKit(lineas).
//
// INC-011 (Juanjo, feedback): prop `costes` = `por_bom` de /api/costes (mapa
// bom_linea_id → coste de sus líneas de OC; ver costePorLineaBom en
// src/services/costes.js). Si viene, columna «Coste» tras «Cant.»: uds de la OC
// × precio unitario de la OC (proveedor = el de la OC), mismo cálculo que la
// pestaña Costes. Sin OC o sin precio: «—». La cabecera de kit suma sus
// componentes y el pie de la tabla da el total. Solo se pasa a quien puede ver
// costes (compras u órdenes, mismo gate que /api/costes): useMatCostes.
//
// INC-001 (Juanjo 8 oct): el material de cada línea es un artículo del catálogo. En
// «Editar» el ID y la descripción ya no se escriben: se elige el artículo (o se crea con
// «Crear artículo», articulo-catalogo.jsx). Las líneas antiguas de texto libre se ven con
// la marca «sin artículo de catálogo»; para editarlas hay que elegir artículo, y el botón
// del enlace (o «Editar») las enlaza. La observación sigue siendo texto libre.
// ─────────────────────────────────────────────────────

const MAT_DIMS = [['largo', 'Largo'], ['ancho', 'Ancho'], ['alto', 'Alto']];
const matFechaLarga = (v) => (v ? String(v).slice(0, 10).split('-').reverse().join('/') : '');
const matNumTxt = (v) => (v == null || v === '' ? null : matNum(v));

// Celda de observación: clic para escribir; se guarda al salir o con Enter
// (Mayús+Enter = salto de línea); Escape descarta.
const MatObsCelda = ({ linea, guardar, onChanged, onToast }) => {
  const [edit, setEdit] = React.useState(false);
  const [txt, setTxt] = React.useState(linea.observacion || '');
  const [busy, setBusy] = React.useState(false);
  React.useEffect(() => { if (!edit) setTxt(linea.observacion || ''); }, [linea.observacion, edit]);
  const ok = async () => {
    if (busy) return;
    if (txt.trim() === String(linea.observacion || '').trim()) { setEdit(false); return; }
    setBusy(true);
    try {
      await guardar(linea, { observacion: txt });
      setEdit(false);
      onChanged && onChanged();
    } catch (e) { onToast && onToast(e.message || 'Error al guardar la observación', true); }
    finally { setBusy(false); }
  };
  if (edit) {
    return (
      <td className="bom-obs">
        <textarea autoFocus value={txt} disabled={busy} onChange={e => setTxt(e.target.value)} onBlur={ok}
          onKeyDown={e => {
            if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); ok(); }
            if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); setTxt(linea.observacion || ''); setEdit(false); }
          }}/>
      </td>
    );
  }
  return (
    <td className="bom-obs" title="Clic para escribir una observación" onClick={() => setEdit(true)}>
      {linea.observacion ? <span style={{whiteSpace:'pre-line'}}>{linea.observacion}</span> : <span className="muted" style={{opacity:.5}}>—</span>}
    </td>
  );
};

// Enviadas: «x / cant» con las uds de los albaranes de entrega vigentes que llevan la línea.
const MatEnviadas = ({ linea }) => {
  const cant = linea.cantidad == null || linea.cantidad === '' ? null : Number(linea.cantidad);
  const env = Number(linea.entregadas) || 0;
  const n = Number(linea.n_albaranes) || 0;
  const marca = !n ? null
    : cant > 0 ? (env >= cant ? ['Completa', 'ok'] : env > 0 ? ['Parcial', 'warn'] : ['Sin uds', ''])
    : env > 0 ? ['Enviada', 'ok'] : ['Sin uds', ''];
  const title = n ? `Albaranes: ${linea.albaranes}${marca[0] === 'Sin uds' ? ' — sin unidades indicadas' : ''}` : 'Sin albarán de entrega';
  if (!n && cant == null) return <span className="muted" title={title}>—</span>;
  return (
    <div title={title} style={{display:'flex',flexDirection:'column',gap:2,alignItems:'flex-start'}}>
      <span className="num" style={{whiteSpace:'nowrap'}}>
        <span className={n ? '' : 'muted'}>{matNum(env)}</span>{cant != null && <span className="muted"> / {matNum(cant)}</span>}
      </span>
      {marca && <span className={`pill ${marca[1]}`}><span className="d"/>{marca[0]}</span>}
      {/* INC-013: lo que queda por enviar (cantidad − enviadas). */}
      {cant > 0 && env < cant && <span className="muted" style={{fontSize:11,whiteSpace:'nowrap'}} title="Cantidad a enviar: lo que falta por salir en albaranes">A enviar: {matNum(cant - env)}</span>}
    </div>
  );
};

// Stock disponible del artículo del catálogo (null = línea de texto libre).
const MatStock = ({ linea }) => {
  const u = linea.unidad || linea.material_unidad || '';
  return (
    <div style={{display:'flex',flexDirection:'column',gap:2,alignItems:'flex-start'}}>
      {linea.stock_disponible == null
        ? <span className="muted" title="Texto libre: no está enlazada a un artículo del catálogo">—</span>
        : <span className="num" style={{whiteSpace:'nowrap',color: linea.stock_disponible < 0 ? 'var(--err)' : linea.stock_disponible === 0 ? 'var(--muted)' : undefined}}
            title="Stock actual del artículo en Almacén">{matNum(linea.stock_disponible)}{u ? ` ${u}` : ''}</span>}
      {linea.de_stock && <span className="pill" title="Sale de stock: ya está en fábrica, no se pide"><span className="d"/>De stock</span>}
    </div>
  );
};

// ── INC-011: coste por línea ──
const matFmtCoste = (v) => `${fmtNumEs(v, 2, 2)} €`;
const matFmtCant = (v) => fmtNumEs(v, 0, 3);

// Carga el coste por línea de materiales de un expediente o proyecto. Devuelve
// null (sin columna) si el usuario no puede ver costes o aún no ha cargado.
// `dep` = lo que obliga a recargar (p. ej. las líneas tras guardar).
function useMatCostes({ expedienteId, proyectoId, dep }) {
  const ver = !!(window.puedeVerCostes && window.puedeVerCostes());
  const [porBom, setPorBom] = React.useState(null);
  const ok = (x) => /^\d+$/.test(String(x == null ? '' : x));
  const url = !ver ? null
    : ok(expedienteId) ? `/api/costes/expediente/${expedienteId}`
    : ok(proyectoId) ? `/api/costes/proyecto/${proyectoId}` : null;
  React.useEffect(() => {
    if (!url) { setPorBom(null); return undefined; }
    let vivo = true;
    // Si falla (p. ej. 403), la tabla sigue como siempre, sin columna de coste.
    API.get(url).then((r) => { if (vivo) setPorBom((r && r.por_bom) || {}); }).catch(() => { if (vivo) setPorBom(null); });
    return () => { vivo = false; };
  }, [url, dep]);
  return porBom;
}
window.useMatCostes = useMatCostes;

// Suma de coste de un conjunto de líneas (cabecera de kit, pie de tabla).
function matSumaCoste(lineas, costes) {
  let importe = 0, conPrecio = 0, conOc = 0;
  for (const l of lineas) {
    const c = costes[l.id];
    if (!c) continue;
    conOc += 1;
    if (c.con_precio) { conPrecio += 1; importe += c.importe; }
  }
  return { importe: Math.round(importe * 100) / 100, conPrecio, conOc, n: lineas.length };
}

const MatCosteCelda = ({ linea, costes }) => {
  const c = costes[linea.id];
  if (!c) return <td className="num right" style={{whiteSpace:'nowrap'}}><span className="muted" title="Sin orden de compra: todavía no tiene coste">—</span></td>;
  const ocs = c.ocs.map((o) => `OC ${o.numero || o.compra_id}: ${o.precio_unitario == null ? 'sin precio' : `${matFmtCant(o.cantidad)} ${o.unidad || ''} × ${matFmtCoste(o.precio_unitario)}`}`).join('\n');
  const title = `${c.proveedores.join(', ')}\n${ocs}`;
  if (!c.con_precio) {
    return <td className="num right" style={{whiteSpace:'nowrap'}}><span className="muted" title={`La línea de la OC no tiene precio: corrígelo en la OC\n${ocs}`}>—</span></td>;
  }
  return (
    <td className="num right" style={{whiteSpace:'nowrap'}} title={title}>
      <div style={{fontWeight:600}}>{matFmtCoste(c.importe)}</div>
      {c.precio_unitario != null && (
        <div className="muted" style={{fontSize:10.5}}>
          {matFmtCant(c.cantidad)}{c.unidad ? ` ${c.unidad}` : ''} × {matFmtCoste(c.precio_unitario)}
        </div>
      )}
      {c.sin_precio > 0 && <div style={{fontSize:10.5,color:'var(--err)'}}>{c.sin_precio} OC sin precio</div>}
    </td>
  );
};

const MatCosteKit = ({ lineas, costes }) => {
  const s = matSumaCoste(lineas, costes);
  if (!s.conPrecio) return <td className="num right"><span className="muted" title="Ningún componente tiene OC con precio">—</span></td>;
  return (
    <td className="num right" style={{whiteSpace:'nowrap'}} title={`Suma de los componentes con OC con precio (${s.conPrecio} de ${s.n})`}>
      <div style={{fontWeight:700}}>{matFmtCoste(s.importe)}</div>
      {s.conPrecio < s.n && <div className="muted" style={{fontSize:10.5}}>{s.conPrecio} de {s.n} con precio</div>}
    </td>
  );
};

// GUT-262: abre la OC de la línea (allí se marca la recepción o se cambia el proveedor).
const matAbrirOc = (id) => window.dispatchEvent(new CustomEvent('erp:open-orden', { detail: { id } }));

// Campos de la fila de edición (valores tal cual de la línea).
const matDraft = (l) => ({
  elemento_id: l.elemento_id || '', proveedor_id: l.proveedor_id || '', material_id: l.material_id || '',
  largo: l.largo ?? '', ancho: l.ancho ?? '', alto: l.alto ?? '',
  cantidad: l.cantidad == null ? '' : Number(l.cantidad), unidad: l.unidad || '',
  ral: l.ral || '', apertura: l.apertura || '', guia: l.guia || '', observacion: l.observacion || '', de_stock: !!l.de_stock,
});

// GUT-252: se puede marcar para «Crear pedido» (sin pedir, no de stock, sin OC).
// INC-001: ni sin artículo del catálogo (comentario pendiente de crear el artículo).
const matPedible = (l) => l.estado === 'pendiente' && !l.de_stock && !l.es_comparativa && !l.oc_id && !!l.material_id;

// INC-001: enlazar una línea sin artículo (comentario o texto libre antiguo) a un artículo
// del catálogo. Propone el artículo con el mismo ID; si no existe, «Crear en Artículos» abre
// la ficha completa en Catálogo de artículos con los datos de la línea y, al guardarla, queda elegido.
const MatEnlazarFila = ({ linea, nCols, guardar, onDone, onCancel, onToast }) => {
  const { items } = useMatCatalogo();
  const [matId, setMatId] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const ref = String(linea.codigo || '').trim().toLowerCase();
  React.useEffect(() => {
    if (matId || !ref) return;
    const hit = items.find((m) => String(m.referencia || '').trim().toLowerCase() === ref);
    if (hit) setMatId(hit.id);
  }, [items]);
  const ok = async () => {
    if (!matId) return;
    setBusy(true);
    try {
      await guardar(linea, { material_id: matId });
      onToast && onToast('Línea enlazada al artículo del catálogo ✓');
      onDone();
    } catch (e) { onToast && onToast(e.message || 'Error al enlazar', true); }
    finally { setBusy(false); }
  };
  return (
    <tr style={{background:'var(--bg-soft, #f9fafb)'}}>
      <td colSpan={nCols} style={{padding:8,whiteSpace:'normal'}}>
        <div style={{display:'flex',alignItems:'end',gap:12,flexWrap:'wrap'}}>
          <div className="field" style={{flex:'1 1 220px'}}>
            <label>Línea sin artículo de catálogo (comentario)</label>
            <div style={{fontSize:12.5,paddingTop:4}}>
              <strong>{linea.nombre || '—'}</strong>
              <span className="muted">{linea.codigo ? ` · ${linea.codigo}` : ''}{linea.unidad ? ` · ${linea.unidad}` : ''}</span>
            </div>
            <div className="muted" style={{fontSize:11}}>Al enlazarla toma el nombre e ID del artículo; el texto actual pasa a la observación.</div>
          </div>
          <div className="field" style={{flex:'2 1 320px'}}>
            <label>Artículo del catálogo *</label>
            <MatArticuloPicker value={matId} onChange={(m) => setMatId(m ? m.id : '')}
              inicial={{ nombre: linea.nombre || '', referencia: linea.codigo || '', unidad: linea.unidad || '' }}/>
          </div>
          <div className="field" style={{display:'flex',gap:6}}>
            <button className="btn primary sm" onClick={ok} disabled={busy || !matId}>{busy ? 'Enlazando…' : 'Enlazar'}</button>
            <button className="btn ghost sm" onClick={onCancel}>Cancelar</button>
          </div>
        </div>
      </td>
    </tr>
  );
};

// INC-001: campo «Artículo» de la fila de edición.
const MatArticuloCampo = ({ linea, draft, set }) => (
  <div className="field" style={{gridColumn:'span 2'}}>
    <label>Artículo del catálogo *</label>
    <MatArticuloPicker value={draft.material_id} onChange={(m) => set('material_id', m ? m.id : '')}
      inicial={{ nombre: linea.nombre || '', referencia: linea.codigo || '', unidad: linea.unidad || '' }}/>
    {!linea.material_id ? (
      <div style={{fontSize:11.5,marginTop:2,color:'var(--warn, #b45309)'}}>
        Línea sin artículo («{linea.nombre || '—'}»): elige el artículo para guardar (si no está, créalo en Artículos). El texto actual pasará a la observación.
      </div>
    ) : String(draft.material_id) === String(linea.material_id) && (
      <div className="muted" style={{fontSize:11,marginTop:2}}>{[linea.material_referencia, linea.material_nombre].filter(Boolean).join(' · ')}</div>
    )}
  </div>
);

const MatTablaFila = ({ linea, enKit, nCols, guardar, borrar, elementos, seleccion, extraCols, mostrarCortina, rowClass, costes, onChanged, onToast }) => {
  const [editing, setEditing] = React.useState(false);
  const [draft, setDraft] = React.useState(() => matDraft(linea));
  const [busy, setBusy] = React.useState(false);
  const provs = window.useMatProveedores();
  // INC-001: panel para enlazar una línea antigua de texto libre a un artículo.
  const [alta, setAlta] = React.useState(false);
  React.useEffect(() => { if (!editing) setDraft(matDraft(linea)); }, [linea, editing]);
  const toast = (m, e) => onToast && onToast(m, e);
  const set = (k, val) => setDraft(d => ({ ...d, [k]: val }));
  const enviado = linea.estado_material === 'enviado';

  const ok = async () => {
    if (!draft.material_id) { toast('Elige el artículo del catálogo (si no está, créalo en Artículos). El texto libre va en la observación.', true); return; }
    setBusy(true);
    try {
      // De stock: misma regla que Demanda (no con OC; no desmarcar si está en proceso/preparado).
      if (draft.de_stock !== !!linea.de_stock) await API.put(`/api/demanda/lineas/${linea.id}/stock`, { de_stock: draft.de_stock });
      const mm = (v) => (v === '' || v == null ? null : v);
      await guardar(linea, {
        // INC-001: el ID y la descripción salen del artículo; solo se manda si cambia.
        ...(String(draft.material_id) !== String(linea.material_id || '') ? { material_id: draft.material_id } : {}),
        largo: mm(draft.largo), ancho: mm(draft.ancho), alto: mm(draft.alto),
        cantidad: mm(draft.cantidad), unidad: draft.unidad,
        ral: draft.ral, apertura: draft.apertura, guia: draft.guia, observacion: draft.observacion,
        ...(linea.oc_id ? {} : { proveedor_id: draft.proveedor_id || null }),
        ...(elementos ? { elemento_id: draft.elemento_id || null } : {}),
      });
      setEditing(false);
      onChanged && onChanged();
    } catch (e) { toast(e.message || 'Error al guardar', true); onChanged && onChanged(); }
    finally { setBusy(false); }
  };

  const eliminar = async () => {
    // GUT-272: lo enviado a obra solo se deshace anulando el albarán.
    if (enviado) { toast(`Enviado en ${linea.albaran_numero}; anula el albarán para deshacerlo`, true); return; }
    if (!confirm('¿Eliminar esta línea? Desaparece del expediente y de Programación.')) return;
    try { await borrar(linea); onChanged && onChanged(); } catch (e) { toast(e.message || 'Error al eliminar', true); }
  };

  const idArticulo = linea.codigo || linea.material_referencia || '';
  const nombreArticulo = linea.nombre || linea.material_nombre || '—';
  const specs = [
    linea.ral ? (/^\s*ral/i.test(linea.ral) ? linea.ral : `RAL ${linea.ral}`) : null,
    linea.apertura ? `Apert. ${linea.apertura}` : null,
    linea.guia ? `Guía ${linea.guia}` : null,
  ].filter(Boolean);
  const fechas = [
    linea.fecha_pedido && `Pedido ${matFechaLarga(linea.fecha_pedido)}`,
    linea.fecha_estimada && !linea.fecha_recepcion && `Llegada estimada ${matFechaLarga(linea.fecha_estimada)}`,
    linea.fecha_recepcion && `Recibido ${matFechaLarga(linea.fecha_recepcion)}`,
    linea.fecha_enviado && `Enviado ${matFechaLarga(linea.fecha_enviado)}`,
  ].filter(Boolean);
  const pedible = seleccion && matPedible(linea);

  if (alta) {
    return <MatEnlazarFila linea={linea} nCols={nCols} guardar={guardar} onToast={onToast}
      onCancel={() => setAlta(false)} onDone={() => { setAlta(false); onChanged && onChanged(); }}/>;
  }

  if (editing) {
    const textoLibre = linea.largo == null && linea.ancho == null && linea.alto == null ? linea.dimensiones : null;
    return (
      <tr style={{background:'var(--bg-soft, #f9fafb)'}}>
        <td colSpan={nCols} style={{padding:8,whiteSpace:'normal'}}>
          <div className="fields-grid" style={{alignItems:'end'}}>
            {elementos && (
              <div className="field">
                <label>Cortina</label>
                <select value={draft.elemento_id || ''} onChange={e => set('elemento_id', e.target.value)}>
                  <option value="">— Sin asignar —</option>
                  {elementos.map(el => <option key={el.id} value={el.id}>{el.nombre ? `${el.codigo} · ${el.nombre}` : el.codigo}</option>)}
                </select>
              </div>
            )}
            <div className="field">
              <label>Proveedor</label>
              {/* GUT-262: si la línea ya está en una OC, el proveedor es el de la OC. */}
              {linea.oc_id ? (
                <div style={{fontSize:12.5,paddingTop:4}}>
                  <strong>{linea.proveedor_nombre || linea.proveedor_texto || 'Sin proveedor'}</strong>
                  <div className="muted" style={{fontSize:11.5}}>
                    Está en la OC {linea.oc_numero} — cámbialo allí.{' '}
                    <a style={{cursor:'pointer'}} onClick={() => matAbrirOc(linea.oc_id)}>Ir a OC {linea.oc_numero}</a>
                  </div>
                </div>
              ) : (
                <select value={draft.proveedor_id || ''} onChange={e => set('proveedor_id', e.target.value)}>
                  <option value="">{linea.proveedor_texto ? `⚠ ${linea.proveedor_texto} (elegir)` : '— Sin proveedor —'}</option>
                  {provs.map(p => <option key={p.id} value={p.id}>{p.label}</option>)}
                </select>
              )}
            </div>
            <MatArticuloCampo linea={linea} draft={draft} set={set}/>
            {MAT_DIMS.map(([k, label]) => (
              <div className="field" key={k}>
                <label>{label} (mm)</label>
                <input type="number" min="0" step="any" value={draft[k]} onChange={e => set(k, e.target.value)}/>
              </div>
            ))}
            {textoLibre && MAT_DIMS.every(([k]) => draft[k] === '' || draft[k] == null) && (
              <div className="field" style={{gridColumn:'1 / -1'}}>
                <div className="muted" style={{fontSize:11.5}}>Dimensiones actuales: «{textoLibre}» (se sustituye al escribir una medida)</div>
              </div>
            )}
            <div className="field"><label>Cantidad</label><input type="number" step="any" value={draft.cantidad} onChange={e => set('cantidad', e.target.value)}/></div>
            <div className="field"><label>Unidad</label><input value={draft.unidad} onChange={e => set('unidad', e.target.value)} placeholder="ud, barra, m…"/></div>
            <div className="field"><label>RAL</label><input value={draft.ral} onChange={e => set('ral', e.target.value)}/></div>
            <div className="field"><label>Apertura</label><input value={draft.apertura} onChange={e => set('apertura', e.target.value)}/></div>
            <div className="field"><label>Guía inferior</label><input value={draft.guia} onChange={e => set('guia', e.target.value)}/></div>
            <div className="field" style={{gridColumn:'1 / -1'}}><label>Observación / comentario</label><input value={draft.observacion} onChange={e => set('observacion', e.target.value)}/></div>
            <div className="field">
              <label style={{display:'flex',alignItems:'center',gap:6,cursor:'pointer'}}
                title={enviado ? `Enviado en ${linea.albaran_numero}; anula el albarán para deshacerlo` : linea.oc_id ? `Ya tiene orden a proveedor (${linea.oc_numero || 'OC'})` : ''}>
                <input type="checkbox" checked={draft.de_stock} disabled={!!linea.oc_id || enviado} onChange={e => set('de_stock', e.target.checked)}/> De stock (ya está en fábrica, no se pide)
              </label>
            </div>
            {/* GUT-262: las fechas de compra no se tocan aquí: las pone la OC al crearla y al recibirla. */}
            <div className="field" style={{gridColumn:'1 / -1'}}>
              <label>Pedido y recepción</label>
              <div className="muted" style={{fontSize:12,paddingTop:2}}>
                {linea.de_stock ? 'De stock: ya está en fábrica, no se pide.'
                  : linea.oc_id ? (
                    <React.Fragment>
                      {`OC ${linea.oc_numero}`}
                      {linea.fecha_pedido ? ` · pedido el ${matFechaLarga(linea.fecha_pedido)}` : ''}
                      {linea.fecha_estimada && !linea.fecha_recepcion ? ` · llegada estimada el ${matFechaLarga(linea.fecha_estimada)}` : ''}
                      {linea.fecha_recepcion ? ` · recibido el ${matFechaLarga(linea.fecha_recepcion)}` : ' · sin recibir: márcalo en la OC o desde el estado de la línea. '}
                      {' '}<a style={{cursor:'pointer'}} onClick={() => matAbrirOc(linea.oc_id)}>Ir a OC {linea.oc_numero}</a>
                    </React.Fragment>
                  ) : linea.fecha_recepcion ? `Recibido el ${matFechaLarga(linea.fecha_recepcion)}.`
                  : 'Sin pedir. Márcala en Expediente › Materiales y pulsa «Crear pedido»: la OC pondrá la fecha de pedido, y al recibirla, la de recepción.'}
                {linea.fecha_enviado ? ` Enviado el ${matFechaLarga(linea.fecha_enviado)} (${linea.albaran_numero}).` : ''}
              </div>
            </div>
            <div className="field" style={{display:'flex',gap:6}}>
              <button className="btn primary sm" onClick={ok} disabled={busy || !draft.material_id} title={draft.material_id ? '' : 'Elige antes el artículo del catálogo'}>{busy ? 'Guardando…' : 'Guardar'}</button>
              <button className="btn ghost sm" onClick={() => setEditing(false)}>Cancelar</button>
            </div>
          </div>
        </td>
      </tr>
    );
  }

  const etiquetas = [
    linea.numero_expediente && `Exp. ${linea.numero_expediente}`,
    mostrarCortina && linea.elemento_codigo,
  ].filter(Boolean);
  const medida = (k) => {
    const v = matNumTxt(linea[k]);
    return <td key={k} className="num" style={{whiteSpace:'nowrap'}}>{v ?? <span className="muted">—</span>}</td>;
  };
  return (
    <tr className={`${rowClass ? rowClass(linea) : ''}${enKit ? ' mat-kit-hijo' : ''}`}>
      {seleccion && (
        <td style={{width:24}}>
          {pedible && <input type="checkbox" checked={seleccion.sel.has(linea.id)} onChange={e => seleccion.marcar(linea.id, e.target.checked)} title="Marcar para crear el pedido"/>}
        </td>
      )}
      {(extraCols || []).map(c => <td key={c.key} {...(c.tdProps || {})}>{c.td(linea)}</td>)}
      <td className="mono" style={{whiteSpace:'normal', ...(enKit ? {paddingLeft:22, boxShadow:'inset 3px 0 var(--accent)'} : {})}}>{idArticulo || '—'}</td>
      <td style={{whiteSpace:'normal',minWidth:160}}>
        {etiquetas.length > 0 && <div className="muted" style={{fontSize:11}}>{etiquetas.join(' · ')}</div>}
        {nombreArticulo}
        {linea.es_comparativa && <span className="pill" style={{marginLeft:6,fontSize:10,opacity:.7}}><span className="d"/>comparativa</span>}
        {!linea.material_id && <MatSinArticulo style={{marginLeft:6}}/>}
        <div className="muted" style={{fontSize:11}}>
          {linea.proveedor_nombre
            || (linea.proveedor_texto
              ? <span title="Venía del Excel y no está en Proveedores: edita la línea y elige uno">⚠ {linea.proveedor_texto}</span>
              : 'Sin proveedor')}
        </div>
        {/* Texto de medidas no numérico (p. ej. «STOCK») que no cabe en Largo/Ancho/Alto. */}
        {linea.dimensiones && linea.largo == null && linea.ancho == null && linea.alto == null && (
          <div className="muted" style={{fontSize:11}} title="Medidas en texto libre: edita la línea para pasarlas a Largo / Ancho / Alto">Medidas: {linea.dimensiones}</div>
        )}
      </td>
      {MAT_DIMS.map(([k]) => medida(k))}
      <td className="num" style={{whiteSpace:'nowrap'}}>{linea.cantidad != null && linea.cantidad !== '' ? `${matNum(linea.cantidad)}${linea.unidad ? ' ' + linea.unidad : ''}` : '—'}</td>
      {costes && <MatCosteCelda linea={linea} costes={costes}/>}
      <td className="muted" style={{whiteSpace:'normal',fontSize:11.5}}>{specs.length ? specs.join(' · ') : '—'}</td>
      <td><MatStock linea={linea}/></td>
      <td><MatEnviadas linea={linea}/></td>
      <td title={fechas.join(' · ')}>
        <MaterialEstado linea={linea} onChanged={onChanged} onToast={onToast}/>
        {/* GUT-262: enlace a la OC de la línea. */}
        {linea.oc_id && (linea.estado_compra || linea.estado) === 'pedido' && (
          <div><a style={{cursor:'pointer',fontSize:11.5}} onClick={() => matAbrirOc(linea.oc_id)} title="Abre la orden a proveedor">OC {linea.oc_numero}</a></div>
        )}
      </td>
      <MatObsCelda linea={linea} guardar={guardar} onChanged={onChanged} onToast={onToast}/>
      <td style={{whiteSpace:'nowrap'}}>
        {!linea.material_id && (
          <button className="btn ghost sm tip" data-tip="Enlazar a un artículo del catálogo" disabled={busy} onClick={() => setAlta(true)}><Icon name="package" size={12}/></button>
        )}
        <button className="btn ghost sm tip" data-tip="Editar" onClick={() => { setDraft(matDraft(linea)); setEditing(true); }}><Icon name="edit" size={12}/></button>
        <button className="btn ghost sm tip" data-tip="Eliminar" style={{color:'var(--err)'}} onClick={eliminar}><Icon name="trash" size={12}/></button>
      </td>
    </tr>
  );
};

// ── INC-009: grupos de kit ──
// Kits desplegados (abiertos) en este navegador; si el almacenamiento falla, todo plegado.
const MAT_KITS_KEY = 'erp.materiales.kitsAbiertos';
const matKitsLeer = () => {
  try { const v = JSON.parse(localStorage.getItem(MAT_KITS_KEY) || '[]'); return new Set(Array.isArray(v) ? v : []); }
  catch (_) { return new Set(); }
};
const useMatKitsAbiertos = () => {
  const [abiertos, setAbiertos] = React.useState(matKitsLeer);
  const alternar = (k) => setAbiertos(prev => {
    const n = new Set(prev); n.has(k) ? n.delete(k) : n.add(k);
    try { localStorage.setItem(MAT_KITS_KEY, JSON.stringify([...n].slice(-300))); } catch (_) { /* sin almacenamiento */ }
    return n;
  });
  return [abiertos, alternar];
};

// Las líneas de un mismo kit, juntas en el sitio de la primera: [{ linea } | { kit, lineas }].
function matAgruparKits(lineas) {
  const out = [];
  const kits = new Map();
  for (const l of lineas) {
    if (!l.kit_grupo) { out.push({ linea: l }); continue; }
    if (!kits.has(l.kit_grupo)) { const it = { kit: l.kit_grupo, lineas: [] }; kits.set(l.kit_grupo, it); out.push(it); }
    kits.get(l.kit_grupo).lineas.push(l);
  }
  return out;
}
const matKitTitulo = (nombre) => (/^kit\b/i.test(String(nombre || '').trim()) ? String(nombre).trim() : `KIT ${String(nombre || '').trim()}`);

// Fila cabecera del kit: clic para plegar / desplegar.
const MatKitFila = ({ lineas, abierto, onToggle, seleccion, extraCols, costes }) => {
  const l0 = lineas[0];
  const reales = lineas.filter(l => !l.es_comparativa);
  const n = reales.length;
  const compra = (l) => l.estado_compra || l.estado;
  const pedidas = reales.filter(l => compra(l) === 'pedido' || compra(l) === 'recibido').length;
  const recibidas = reales.filter(l => compra(l) === 'recibido').length;
  const enviadas = reales.filter(l => l.estado_material === 'enviado').length;
  const pedibles = seleccion ? lineas.filter(matPedible) : [];
  const marcadas = pedibles.filter(l => seleccion.sel.has(l.id)).length;
  const casillaRef = React.useRef(null);
  React.useEffect(() => { if (casillaRef.current) casillaRef.current.indeterminate = marcadas > 0 && marcadas < pedibles.length; });
  const provs = [...new Set(lineas.map(l => l.proveedor_nombre || l.proveedor_texto).filter(Boolean))];
  const kits = Number(l0.kit_cantidad) || null;
  const pill = (x, label, cls) => (
    <span className={`pill ${x === n ? 'ok' : x > 0 ? cls : ''}`} style={{whiteSpace:'nowrap'}} title={`${label}: ${x} de ${n} componentes`}>
      <span className="d"/>{label} {x}/{n}
    </span>
  );
  const parar = (e) => e.stopPropagation();
  return (
    <tr className="mat-kit-cab" onClick={onToggle} style={{cursor:'pointer',background:'var(--bg-elev, #fbfbf9)'}}
      title={abierto ? 'Clic para plegar el kit' : 'Clic para ver los componentes del kit'}>
      {seleccion && (
        <td style={{width:24}} onClick={parar}>
          {pedibles.length > 0 && (
            <input ref={casillaRef} type="checkbox" checked={marcadas === pedibles.length}
              title={`Marcar los ${pedibles.length} componentes sin pedir del kit para crear el pedido`}
              onChange={e => pedibles.forEach(l => seleccion.marcar(l.id, e.target.checked))}/>
          )}
        </td>
      )}
      {(extraCols || []).map(c => <td key={c.key} {...(c.tdProps || {})} onClick={c.tdKit ? parar : undefined}>{c.tdKit ? c.tdKit(lineas) : null}</td>)}
      <td style={{whiteSpace:'nowrap'}}>
        <button type="button" className="btn ghost sm" style={{padding:'0 5px',minWidth:0}} aria-expanded={abierto}
          aria-label={abierto ? 'Plegar kit' : 'Desplegar kit'} onClick={e => { e.stopPropagation(); onToggle(); }}>
          <span style={{display:'inline-block',transition:'transform .15s',transform: abierto ? 'rotate(90deg)' : 'none'}}>▸</span>
        </button>
        <span className="pill info" style={{fontSize:10.5,marginLeft:4}}><span className="d"/>Kit</span>
      </td>
      <td colSpan={4} style={{whiteSpace:'normal'}}>
        <div className="strong" style={{fontWeight:700}}>{matKitTitulo(l0.kit_nombre)}</div>
        <div className="muted" style={{fontSize:11}}>
          {n} componente{n === 1 ? '' : 's'}
          {provs.length > 0 && ` · ${provs.length === 1 ? provs[0] : `${provs.length} proveedores`}`}
          {!abierto && ' · clic para ver'}
        </div>
      </td>
      <td className="num" style={{whiteSpace:'nowrap'}}>{kits ? `${matNum(kits)} kit${kits === 1 ? '' : 's'}` : <span className="muted">—</span>}</td>
      {costes && <MatCosteKit lineas={lineas} costes={costes}/>}
      <td></td><td></td>
      <td>{pill(enviadas, 'Enviado', 'warn')}</td>
      <td>
        <div style={{display:'flex',flexDirection:'column',gap:2,alignItems:'flex-start'}}>
          {pill(pedidas, 'Pedido', 'info')}
          {pedidas > 0 && pill(recibidas, 'Recibido', 'info')}
        </div>
      </td>
      <td></td>
      <td style={{whiteSpace:'nowrap'}} className="muted">{abierto ? 'Plegar' : 'Ver'}</td>
    </tr>
  );
};

const MatTabla = ({ grupos, className = 'tbl tbl-compact', style, vacio = 'Sin materiales', ...fila }) => {
  const extra = fila.extraCols || [];
  const costes = fila.costes || null;
  const antesCoste = (fila.seleccion ? 1 : 0) + extra.length + 6; // … ID, Descripción, L, A, H, Cant.
  const nCols = antesCoste + 6 + (costes ? 1 : 0);
  const total = grupos.reduce((n, g) => n + g.lineas.length, 0);
  const sumaCoste = costes ? matSumaCoste(grupos.flatMap(g => g.lineas), costes) : null;
  const [abiertos, alternar] = useMatKitsAbiertos();
  return (
    <table className={className} style={{fontSize:12.5, ...(style || {})}}>
      <thead>
        <tr>
          {fila.seleccion && <th></th>}
          {extra.map(c => <th key={c.key} {...(c.thProps || {})}>{c.th}</th>)}
          <th>ID</th><th>Descripción</th>
          <th className="num" title="Largo (mm)">Largo</th><th className="num" title="Ancho (mm)">Ancho</th><th className="num" title="Alto (mm)">Alto</th>
          <th className="num" title="Cantidad">Cant.</th>
          {costes && <th className="num" style={{textAlign:'right'}} title="Uds de la orden de compra × precio unitario de la OC (proveedor = el de la OC). «—» = sin OC o la OC no tiene precio">Coste</th>}
          <th title="RAL · apertura · guía inferior">Specs</th>
          <th title="Stock actual del artículo del catálogo (Almacén). «—» = línea antigua sin artículo de catálogo">Stock</th>
          <th title="Cant. enviadas a obra en albaranes de entrega vigentes / cantidad de la línea, y lo que queda a enviar">Enviadas · a enviar</th>
          <th>Estado</th>
          <th title="Clic en la celda para escribir">Observación</th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        {total === 0 && <tr><td colSpan={nCols} className="muted" style={{textAlign:'center'}}>{vacio}</td></tr>}
        {grupos.map(g => (
          <React.Fragment key={g.key}>
            {g.titulo && g.lineas.length > 0 && (
              <tr><td colSpan={nCols} className="muted" style={{background:'var(--bg-soft, #f9fafb)',fontWeight:600,fontSize:11,letterSpacing:.4}}>{g.titulo}</td></tr>
            )}
            {matAgruparKits(g.lineas).map(it => (it.linea
              ? <MatTablaFila key={it.linea.id} linea={it.linea} nCols={nCols} {...fila}/>
              : (
                <React.Fragment key={`kit-${it.kit}`}>
                  <MatKitFila lineas={it.lineas} abierto={abiertos.has(it.kit)} onToggle={() => alternar(it.kit)}
                    seleccion={fila.seleccion} extraCols={extra} costes={costes}/>
                  {abiertos.has(it.kit) && it.lineas.map(l => <MatTablaFila key={l.id} linea={l} enKit nCols={nCols} {...fila}/>)}
                </React.Fragment>
              )))}
          </React.Fragment>
        ))}
      </tbody>
      {sumaCoste && total > 0 && (
        <tfoot>
          <tr className="mat-coste-pie">
            <td colSpan={antesCoste} style={{textAlign:'right'}}>
              Coste <span className="muted" style={{fontWeight:400}}>· {sumaCoste.conPrecio} de {sumaCoste.n} línea{sumaCoste.n === 1 ? '' : 's'} con OC con precio</span>
            </td>
            <td className="num right" style={{whiteSpace:'nowrap'}}>{matFmtCoste(sumaCoste.importe)}</td>
            <td colSpan={6}></td>
          </tr>
        </tfoot>
      )}
    </table>
  );
};

window.MatTabla = MatTabla;
