// ─────────────────────────────────────────────────────
// PROGRAMACIÓN — sustituye PROGRAMACIÓN.xlsm. Tabla de proyectos editable en
// línea (cada celda se guarda al instante), calendario por instalador, carga
// mensual, materiales pendientes, portes, tareas de fábrica y configuración
// (instaladores + festivos). Multiusuario: la vista se refresca sola cada 30 s
// y al volver a la pestaña, salvo que haya una celda en edición.
// ─────────────────────────────────────────────────────

// GUT-249: dos columnas de estado (lista de Víctor). El general es obligatorio y manda en
// "activos" / KPIs; el estado es el siguiente paso y puede ir vacío ("ningún estado").
const PG_ESTADOS_GENERAL = [['EN CURSO', 'info'], ['REPASO', 'err'], ['TERMINADO', 'ok']];
const PG_GENERAL_CLS = Object.fromEntries(PG_ESTADOS_GENERAL);
const PG_ESTADOS = [
  ['MEDICIÓN', ''], ['DIBUJO', ''], ['REV. DIBUJO', ''], ['MATERIALES', 'warn'], ['PREPARACIÓN', 'warn'],
  ['PORTE', 'warn'], ['INSTALACIÓN', 'glass'], ['TERMINADO PDC', 'ok'], ['PDTE CLIENTE', ''], ['PARALIZADO', ''],
];
const PG_ESTADO_CLS = Object.fromEntries(PG_ESTADOS);
// Pasos del flujo de obra: al pasar el general a TERMINADO se vacían (los otros tres se conservan).
const PG_PASOS_OBRA = ['MEDICIÓN', 'DIBUJO', 'REV. DIBUJO', 'MATERIALES', 'PREPARACIÓN', 'PORTE', 'INSTALACIÓN'];
// Cambio coherente de las dos columnas: TERMINADO limpia el paso; TERMINADO PDC fuerza general TERMINADO.
const pgCambioEstado = (r, body) => {
  if (body.estado_general === 'TERMINADO' && PG_PASOS_OBRA.includes(r.estado)) return { ...body, estado: null };
  if (body.estado === 'TERMINADO PDC' && r.estado_general !== 'TERMINADO') return { ...body, estado_general: 'TERMINADO' };
  return body;
};
// INC-005 (reabierta): proceso que marca la oficina; da el color en Programación › Mapa.
const PG_PROCESOS = [
  ['pendiente_medida', 'Pendiente de medida', '#2563eb'],
  ['pendiente_revision', 'Pendiente revisión instalación', '#9333ea'],
  ['remate', 'Remate', '#ea580c'],
  ['garantia', 'Garantía', '#dc2626'],
];
const PG_PRIORIDADES = [['MUY ALTA', 'err'], ['ALTA', 'err'], ['MEDIA', 'warn'], ['NORMAL', ''], ['BAJA', '']];
const PG_PRIORIDAD_CLS = Object.fromEntries(PG_PRIORIDADES);
// Tipos de fecha del proyecto. Portes y fabricación NO son tipos de fecha: viven en las pestañas
// Portes y Tareas fábrica, y salen en el calendario por su cuenta (filas "Portes" y "Fábrica").
const PG_TIPOS_EVENTO = { instalacion: 'Instalación', medicion: 'Medición', repaso: 'Repaso', otro: 'Otro' };
const PG_TIPOS_CAL = { ...PG_TIPOS_EVENTO, fabricacion: 'Fabricación', porte: 'Porte', prevista: 'Fecha cliente' };
// GUT-245: aviso sobre la fecha que pide el cliente. Devuelve [clase, texto] o null.
// Rojo: ya pasó y no hay instalación planificada ni hecha. Ámbar: faltan ≤ 15 días sin
// planificar, o la instalación planificada termina después de lo que pidió el cliente.
const pgAvisoCliente = (r) => {
  if (!r.fecha_prevista_cliente || r.fecha_instalacion_real) return null;
  const hoy = pgToday();
  if (!r.fecha_inicio) {
    if (r.fecha_prevista_cliente < hoy) return ['err', 'La fecha que pidió el cliente ya pasó y no hay instalación planificada'];
    if (r.fecha_prevista_cliente <= pgAddDays(hoy, 15)) return ['warn', 'Quedan 15 días o menos para la fecha que pidió el cliente y no hay instalación planificada'];
    return null;
  }
  if (r.fecha_fin && r.fecha_fin > r.fecha_prevista_cliente) return ['warn', `La instalación planificada termina el ${pgFmtLong(r.fecha_fin)}, después de la fecha que pidió el cliente`];
  return null;
};
const PG_DIAS = ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'];
const PG_MESES = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'];

const pgToday = () => new Date().toISOString().slice(0, 10);
const pgAddDays = (iso, n) => { const d = new Date(`${iso}T00:00:00Z`); d.setUTCDate(d.getUTCDate() + n); return d.toISOString().slice(0, 10); };
const pgMonday = (iso) => { const d = new Date(`${iso}T00:00:00Z`); const dow = (d.getUTCDay() + 6) % 7; return pgAddDays(iso, -dow); };
const pgDow = (iso) => (new Date(`${iso}T00:00:00Z`).getUTCDay() + 6) % 7; // 0 = lunes
// cal = { festivos: Set<'YYYY-MM-DD'>, dias: Set<0..6 según getUTCDay, 0 = domingo> }
const pgOff = (iso, cal) => cal.festivos.has(iso) || !cal.dias.has((pgDow(iso) + 1) % 7);
// ¿Se pinta el trabajo `it` el día `d`? Todos los días del rango, también fines de semana y festivos:
// si el trabajo abarca ese día, se trabaja (y Carga lo cuenta como asignado).
const pgPinta = (it, d) => d >= it.fecha_inicio && d <= it.fecha_fin;
const PG_DIAS_SEMANA = [[1, 'Lun'], [2, 'Mar'], [3, 'Mié'], [4, 'Jue'], [5, 'Vie'], [6, 'Sáb'], [0, 'Dom']];
const pgFmt = (iso) => (iso ? `${iso.slice(8, 10)}/${iso.slice(5, 7)}/${iso.slice(2, 4)}` : '—');
const pgFmtLong = (iso) => (iso ? `${iso.slice(8, 10)}/${iso.slice(5, 7)}/${iso.slice(0, 4)}` : '—');
const pgMesLabel = (ym) => { const [y, m] = ym.split('-'); return `${PG_MESES[Number(m) - 1]} ${y}`; };
const pgIsInput = (el) => el && ['INPUT', 'SELECT', 'TEXTAREA'].includes(el.tagName);

// Refresco automático mientras nadie está escribiendo en la vista.
function usePgAutoRefresh(reload, ms = 30000) {
  React.useEffect(() => {
    const tick = () => { if (document.visibilityState === 'visible' && !pgIsInput(document.activeElement)) reload(); };
    const id = setInterval(tick, ms);
    const onFocus = () => tick();
    window.addEventListener('focus', onFocus);
    document.addEventListener('visibilitychange', onFocus);
    return () => { clearInterval(id); window.removeEventListener('focus', onFocus); document.removeEventListener('visibilitychange', onFocus); };
  }, [reload, ms]);
}

// ── Celda editable: guarda al salir (texto/fecha/número) o al cambiar (select/check) ──
// Los textarea crecen con el contenido (nada queda recortado); `multiline` = texto de una
// línea que se convierte en textarea de 1 fila y se expande al escribir (notas en tablas).
const pgAutoAlto = (el) => { if (!el) return; el.style.height = 'auto'; el.style.height = `${el.scrollHeight + 2}px`; };
const PgEdit = ({ type = 'text', value, options, onSave, className = '', placeholder, style, title, multiline, sinVacio, list }) => {
  const [v, setV] = React.useState(value == null ? '' : value);
  React.useEffect(() => { setV(value == null ? '' : value); }, [value]);
  const ref = React.useRef(null);
  const esArea = type === 'textarea' || multiline;
  React.useLayoutEffect(() => { if (esArea) pgAutoAlto(ref.current); }, [v, esArea]);
  React.useEffect(() => {
    // Al cambiar el ancho (ampliar/reducir el panel, redimensionar ventana) se recalcula la altura.
    if (!esArea || !ref.current || typeof ResizeObserver === 'undefined') return;
    let w = ref.current.offsetWidth;
    const ro = new ResizeObserver(() => { const el = ref.current; if (el && el.offsetWidth !== w) { w = el.offsetWidth; pgAutoAlto(el); } }); ro.observe(ref.current);
    return () => ro.disconnect();
  }, [esArea]);
  const stop = (e) => e.stopPropagation();
  if (type === 'select') {
    return (
      <select className={`pg-in ${className}`} value={value == null ? '' : value} style={style} title={title}
        onClick={stop} onChange={(e) => onSave(e.target.value === '' ? null : e.target.value)}>
        {!sinVacio && <option value="">—</option>}
        {options.map((o) => { const [val, lbl] = Array.isArray(o) ? o : [o, o]; return <option key={val} value={val}>{lbl}</option>; })}
      </select>
    );
  }
  if (type === 'check') {
    return <input type="checkbox" className="pg-ck" checked={!!value} onClick={stop} onChange={(e) => onSave(e.target.checked)} title={title} />;
  }
  const commit = () => { const cur = value == null ? '' : String(value); if (String(v) !== cur) onSave(v === '' ? null : v); };
  const Tag = esArea ? 'textarea' : 'input';
  return (
    <Tag ref={ref} className={`pg-in ${esArea ? 'pg-area' : ''} ${multiline ? 'pg-cell-area' : ''} ${v === '' ? 'pg-empty' : ''} ${className}`} type={esArea ? undefined : type === 'num' ? 'number' : type} value={v} placeholder={placeholder} style={style} title={title}
      step={type === 'num' ? 'any' : undefined} rows={type === 'textarea' ? 3 : esArea ? 1 : undefined} list={esArea ? undefined : list}
      onClick={stop} onChange={(e) => setV(e.target.value)} onBlur={commit}
      onKeyDown={(e) => { if (e.key === 'Enter' && (!esArea || (multiline && !e.shiftKey))) { e.preventDefault(); e.target.blur(); } if (e.key === 'Escape') { setV(value == null ? '' : value); e.target.blur(); } }} />
  );
};

const PgField = ({ label, children, full }) => <div className={`field ${full ? 'full' : ''}`}><label>{label}</label>{children}</div>;
// ── Selector con buscador para listas largas. items = [{ id, label, mono?, sub? }] ──
// multi: value es un array de ids; cada clic marca/desmarca y el panel se queda abierto.
const PgPicker = ({ value, items, onChange, placeholder = 'Elegir…', buscar = 'Buscar…', vacio = '— Ninguno —', allowEmpty, compact, style, multi }) => {
  const [open, setOpen] = React.useState(false);
  const [q, setQ] = React.useState('');
  const [idx, setIdx] = React.useState(0);
  const [pos, setPos] = React.useState({ left: 0, top: 0, width: 320, maxHeight: 360 });
  const btnRef = React.useRef(null);
  const ddRef = React.useRef(null);
  const all = items || [];
  const vals = multi ? (value || []).map(String) : [];
  const esSel = (p) => (multi ? vals.includes(String(p.id)) : String(p.id) === String(value ?? ''));
  const selMulti = multi ? vals.map((v) => all.find((p) => String(p.id) === v) || { id: v, label: v }) : [];
  const sel = multi ? (selMulti.length ? { label: selMulti.map((p) => p.label).join(', ') } : null)
    : all.find((p) => String(p.id) === String(value ?? '')) || (value ? { id: value, label: String(value) } : null);
  const list = React.useMemo(() => {
    const t = q.trim().toLowerCase();
    const arr = t ? all.filter((p) => [p.mono, p.label, p.sub].some((v) => String(v || '').toLowerCase().includes(t))) : all;
    return arr.slice(0, 60);
  }, [q, items]);
  const abrir = (e) => {
    e.stopPropagation();
    const r = btnRef.current.getBoundingClientRect();
    const width = Math.max(r.width, 340);
    const left = Math.max(8, Math.min(r.left, window.innerWidth - width - 8));
    // Se abre hacia donde haya más sitio (abajo o arriba) y ocupa hasta esa altura.
    const abajo = window.innerHeight - r.bottom - 8;
    const arriba = r.top - 8;
    const haciaArriba = abajo < 360 && arriba > abajo;
    setPos(haciaArriba
      ? { left, bottom: window.innerHeight - r.top + 2, width, maxHeight: Math.min(480, arriba) }
      : { left, top: r.bottom + 2, width, maxHeight: Math.min(480, abajo) });
    setQ(''); setIdx(0); setOpen(true);
  };
  const elegir = (p) => {
    if (multi) {
      if (!p) { onChange([]); return; }
      const id = String(p.id);
      onChange(vals.includes(id) ? vals.filter((v) => v !== id) : [...vals, id]);
      return;
    }
    onChange(p ? String(p.id) : null); setOpen(false);
  };
  React.useEffect(() => {
    if (!open) return;
    const fuera = (e) => { if (!ddRef.current?.contains(e.target) && !btnRef.current?.contains(e.target)) setOpen(false); };
    const esc = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', fuera); document.addEventListener('keydown', esc);
    // Scroll fuera del desplegable lo cierra (el botón se movería); dentro de la lista, no.
    const scroll = (e) => { if (!ddRef.current?.contains(e.target)) setOpen(false); };
    window.addEventListener('scroll', scroll, true);
    return () => { document.removeEventListener('mousedown', fuera); document.removeEventListener('keydown', esc); window.removeEventListener('scroll', scroll, true); };
  }, [open]);
  const teclas = (e) => {
    if (e.key === 'ArrowDown') { e.preventDefault(); setIdx((i) => Math.min(list.length - 1, i + 1)); }
    else if (e.key === 'ArrowUp') { e.preventDefault(); setIdx((i) => Math.max(0, i - 1)); }
    else if (e.key === 'Enter') { e.preventDefault(); if (list[idx]) elegir(list[idx]); }
  };
  return (
    <>
      <button type="button" ref={btnRef} className={`pg-picker-btn ${compact ? 'compact' : ''} ${sel ? '' : 'empty'}`} style={style} onClick={abrir} title={sel ? [sel.mono, sel.label].filter(Boolean).join(' · ') : ''}>
        <span className="truncate">{sel ? <>{sel.mono && <><span className="mono">{sel.mono}</span> · </>}{sel.label}</> : placeholder}</span>
        <Icon name="chevron_down" size={12} />
      </button>
      {open && ReactDOM.createPortal(
        // En el body: dentro del drawer (transform) un position:fixed se calcula mal y se recorta.
        <div ref={ddRef} className="pg-picker-dd" style={pos} onClick={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()}>
          <div className="pg-picker-q"><Icon name="search" size={13} /><input autoFocus placeholder={buscar} value={q} onChange={(e) => { setQ(e.target.value); setIdx(0); }} onKeyDown={teclas} /></div>
          <div className="pg-picker-list">
            {allowEmpty && <div className="pg-picker-it muted" onClick={() => elegir(null)}>{vacio}</div>}
            {list.length === 0 && <div className="pg-picker-it muted">Sin resultados</div>}
            {list.map((p, i) => (
              <div key={p.id} className={`pg-picker-it ${i === idx ? 'on' : ''} ${esSel(p) ? 'sel' : ''}`} onMouseEnter={() => setIdx(i)} onClick={() => elegir(p)}>
                {multi && <span className={`pg-picker-ck ${esSel(p) ? 'on' : ''}`}>{esSel(p) ? '✓' : ''}</span>}
                {p.mono && <span className="mono strong">{p.mono}</span>}
                <span className="truncate" style={{ flex: 1 }}>{p.label}</span>
                {p.sub && <span className="muted" style={{ fontSize: 11 }}>{p.sub}</span>}
              </div>
            ))}
            {all.length > list.length && q.trim() === '' && <div className="pg-picker-it muted" style={{ fontSize: 11 }}>Mostrando {list.length} de {all.length} · escribe para filtrar</div>}
          </div>
        </div>,
        document.body,
      )}
    </>
  );
};

const PgProyectoPicker = ({ proyectos, ...rest }) => {
  const items = React.useMemo(() => (proyectos || []).map((p) => ({ id: p.id, mono: p.numero, label: p.nombre.replace(/^\d+[.,]?\s*/, ''), sub: p.ubicacion })), [proyectos]);
  return <PgPicker items={items} placeholder="Proyecto…" buscar="Buscar por nº, nombre o ubicación…" vacio="— Sin proyecto —" {...rest} />;
};

// Persona elegida entre los instaladores por nombre (responsable de un porte).
const PgInstaladorPicker = ({ instaladores, placeholder = 'Instalador…', vacio = '— Ninguno —', ...rest }) => {
  const items = React.useMemo(() => (instaladores || []).filter((i) => i.activo).map((i) => ({ id: i.nombre, label: i.nombre, sub: i.tipo === 'subcontrata' ? 'Subcontrata' : '' })), [instaladores]);
  return <PgPicker items={items} placeholder={placeholder} buscar="Buscar instalador…" vacio={vacio} allowEmpty {...rest} />;
};
const PgResponsablePicker = (props) => <PgInstaladorPicker placeholder="Responsable…" vacio="— Sin responsable —" {...props} />;
// Varias personas por trabajo (ids): instaladores de un proyecto/fecha, operarios de una tarea.
// Los dados de baja solo aparecen si ya estaban asignados.
// Filtro de personas que se recuerda por navegador: cada uno deja puesto su nombre y solo ve lo suyo.
const usePgFiltroPersonas = (key) => {
  const [v, setV] = React.useState(() => { try { const x = JSON.parse(localStorage.getItem(key) || '[]'); return Array.isArray(x) ? x.map(String) : []; } catch { return []; } });
  React.useEffect(() => { try { localStorage.setItem(key, JSON.stringify(v)); } catch {} }, [key, v]);
  return [v, setV];
};
const pgPersonaOpts = (instaladores, sin = 'Sin asignar') => [{ value: 'sin', label: sin }, ...instaladores.filter((i) => i.activo).map((i) => ({ value: String(i.id), label: i.nombre }))];

const PgPersonasPicker = ({ instaladores, value, placeholder = 'Sin asignar', vacio = '— Quitar a todos —', ...rest }) => {
  const items = React.useMemo(() => {
    const sel = new Set((value || []).map(String));
    return (instaladores || []).filter((i) => i.activo || sel.has(String(i.id))).map((i) => ({ id: i.id, label: i.nombre + (i.activo ? '' : ' (baja)'), sub: i.tipo === 'subcontrata' ? 'Subcontrata' : '' }));
  }, [instaladores, value]);
  return <PgPicker multi items={items} value={value || []} placeholder={placeholder} buscar="Buscar persona…" vacio={vacio} allowEmpty {...rest} />;
};

const PgPill = ({ v, map }) => v ? <span className={`pill ${map[v] || ''}`}><span className="d" />{v}</span> : <span className="muted">—</span>;

// ═══════════════════════════════════════════════════════
// Vista principal
// ═══════════════════════════════════════════════════════
const ProgramacionView = ({ onToast }) => {
  const [tab, setTab] = React.useState(() => { try { return localStorage.getItem('pg_tab') || 'proyectos'; } catch { return 'proyectos'; } });
  const [instaladores, setInstaladores] = React.useState([]);
  const [proyectos, setProyectos] = React.useState([]);
  const [openId, setOpenId] = React.useState(null);
  const [openTab, setOpenTab] = React.useState(null); // INC-018: pestaña con la que se abre la ficha
  const [tick, setTick] = React.useState(0);
  const refresh = React.useCallback(() => setTick((t) => t + 1), []);

  React.useEffect(() => { try { localStorage.setItem('pg_tab', tab); } catch {} }, [tab]);
  // INC-018: abrir la ficha de un proyecto desde otra vista (p. ej. clic en el cliente → Materiales).
  React.useEffect(() => {
    const abrir = (p) => { if (p && p.id) { setOpenId(Number(p.id)); setOpenTab(p.tab || null); } };
    abrir(window.__erpPendingProyecto); window.__erpPendingProyecto = null;
    const handler = (e) => { abrir(e.detail); setTimeout(() => { window.__erpPendingProyecto = null; }, 0); };
    window.addEventListener('erp:open-proyecto', handler);
    return () => window.removeEventListener('erp:open-proyecto', handler);
  }, []);
  const abrirProyecto = React.useCallback((id) => { setOpenId(id); setOpenTab(null); }, []);
  React.useEffect(() => {
    API.get('/api/programacion/instaladores').then(setInstaladores).catch(() => {});
    API.get('/api/programacion/proyectos').then(setProyectos).catch(() => {});
  }, [tick]);

  const TABS = [['proyectos', 'Proyectos'], ['mapa', 'Mapa'], ['calendario', 'Calendario'], ['carga', 'Carga'], ['materiales', 'Materiales'], ['portes', 'Portes'], ['tareas', 'Tareas'], ['config', 'Configuración']];
  const common = { onToast, instaladores, proyectos, refresh, tick, onOpen: abrirProyecto };
  const tabLabel = (TABS.find(([k]) => k === tab) || [])[1] || '';
  // Imprimir la pantalla actual (o la ficha abierta). El CSS @media print oculta menú, filtros y edición.
  const imprimir = () => { document.body.classList.add('pg-printing'); const off = () => document.body.classList.remove('pg-printing'); window.addEventListener('afterprint', off, { once: true }); setTimeout(() => window.print(), 50); };

  return (
    <div className="view-wrap pg-view">
      <div className="page-h">
        <div>
          <h1>Programación</h1>
          <div className="sub">Proyectos, fechas e instaladores. Todo el mundo ve los cambios al instante.</div>
        </div>
        <div className="page-h-actions">
          {tab !== 'config' && <button className="btn" onClick={imprimir} title="Imprimir esta pantalla (o la ficha abierta)"><Icon name="printer" /> Imprimir</button>}
        </div>
      </div>
      <div className="pg-print-head">Programación · {tabLabel} · {new Date().toLocaleDateString('es-ES')}</div>
      <div className="subnav">
        {TABS.map(([k, l]) => <button key={k} className={tab === k ? 'on' : ''} onClick={() => setTab(k)}>{l}</button>)}
      </div>
      {tab === 'proyectos' && <PgProyectosTab {...common} />}
      {tab === 'mapa' && window.PgMapaTab && <window.PgMapaTab {...common} />}
      {tab === 'calendario' && <PgCalendarioTab {...common} />}
      {tab === 'carga' && <PgCargaTab {...common} />}
      {tab === 'materiales' && <PgMaterialesTab {...common} />}
      {tab === 'portes' && <PgPortesTab {...common} />}
      {tab === 'tareas' && <PgTareasTab {...common} />}
      {tab === 'config' && <PgConfigTab {...common} />}
      {openId && <PgProyectoDrawer key={`${openId}-${openTab || ''}`} id={openId} initialTab={openTab} onClose={() => setOpenId(null)} onToast={onToast} instaladores={instaladores} onChanged={refresh} />}
    </div>
  );
};

// ═══════════════════════════════════════════════════════
// Pestaña Proyectos
// ═══════════════════════════════════════════════════════
// INC-014: columna MATERIALES calculada de bom_lineas en el servidor.
// OK = todas las líneas preparadas (las enviadas al completo cuentan como preparadas).
// Ajuste 19 oct (Juanjo): si falta algo, se dice qué (lacado, corte, compra, por preparar)
// y cuántas líneas han salido solo en parte (envíos parciales).
const PG_MAT_FILTRO = [{ value: 'COMPLETO', label: 'OK (todo preparado)' }, { value: 'INCOMPLETO', label: 'Falta material' }, { value: 'SIN', label: 'Sin materiales' }];
// Para ordenar: fracción preparada (0..1); sin materiales va al final (null).
const pgMatAvance = (r) => (r.materiales_total ? (r.materiales_total - r.materiales_pendientes) / r.materiales_total : null);
// Lo que falta, en el orden en que se ve en fábrica. [clave, texto corto, texto del tooltip]
const pgMatFaltas = (r) => {
  const pend = r.materiales_pendientes || 0;
  const lac = r.materiales_lacado || 0; const cor = r.materiales_corte || 0; const com = r.materiales_compra || 0;
  // Las que no esperan compra ni proceso: recibidas (o de stock) sin marcar «Preparado».
  const resto = Math.max(0, pend - Math.max(lac, cor, com));
  return [
    ['lacado', `Lacado ${lac}`, `${lac} pendiente${lac === 1 ? '' : 's'} de lacado`, lac],
    ['corte', `Corte ${cor}`, `${cor} pendiente${cor === 1 ? '' : 's'} de corte`, cor],
    ['compra', `Compra ${com}`, `${com} sin recibir del proveedor`, com],
    ['preparar', `Preparar ${resto}`, `${resto} sin marcar como preparada${resto === 1 ? '' : 's'}`, resto],
  ].filter((x) => x[3] > 0);
};
const PgMatEstadoCell = ({ r }) => {
  if (!r.materiales_total) return <span className="muted" title="El proyecto no tiene líneas de material">—</span>;
  const prep = r.materiales_total - r.materiales_pendientes;
  const ok = r.materiales_estado === 'COMPLETO';
  const par = r.materiales_parciales || 0;
  const env = r.materiales_enviadas || 0;
  const faltas = ok ? [] : pgMatFaltas(r);
  const title = [
    `${prep} de ${r.materiales_total} líneas preparadas (o ya enviadas)`,
    ...faltas.map((f) => f[2]),
    env ? `${env} enviada${env === 1 ? '' : 's'} al completo` : null,
    par ? `${par} con envío parcial (han salido algunas uds)` : null,
  ].filter(Boolean).join('\n');
  return (
    <div title={title} style={{ lineHeight: 1.25 }}>
      <span className={`pill ${ok ? 'ok' : 'warn'}`} style={{ whiteSpace: 'nowrap' }}>
        {ok ? 'OK' : 'FALTA'} <span style={{ fontSize: 10, opacity: 0.8 }}>{prep}/{r.materiales_total}</span>
      </span>
      {(faltas.length > 0 || par > 0) && (
        <div className="muted" style={{ fontSize: 10.5, marginTop: 2, whiteSpace: 'normal' }}>
          {faltas.map((f) => f[1]).concat(par ? [`${par} env. parcial`] : []).join(' · ')}
        </div>
      )}
    </div>
  );
};
// INC-019: filtro «Transporte»: con / sin envío y por agencia (las ya usadas).
const pgEnvioFiltro = (agencias) => [{ value: 'SI', label: 'Con envío' }, { value: 'NO', label: 'Sin envío' }]
  .concat((agencias || []).map((a) => ({ value: `AG:${a}`, label: `Agencia ${a}` })));
const PgProyectosTab = ({ onToast, instaladores, refresh, tick, onOpen }) => {
  const [rows, setRows] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [search, setSearch] = React.useState('');
  const [general, setGeneral] = React.useState([]);
  const [estado, setEstado] = React.useState([]);
  const [inst, setInst] = React.useState([]);
  const [prov, setProv] = React.useState([]);
  const [prio, setPrio] = React.useState([]);
  const [mats, setMats] = React.useState([]); // INC-014
  const [envios, setEnvios] = React.useState([]); // INC-019
  const [soloActivos, setSoloActivos] = React.useState(true);
  const [filtros, setFiltros] = React.useState({ provincias: [] });
  const [sort, setSort] = React.useState({ k: 'fecha_inicio', d: 'desc' });
  const [nuevo, setNuevo] = React.useState(false);
  const [updatedAt, setUpdatedAt] = React.useState(null);

  const params = React.useMemo(() => {
    const p = {};
    if (search) p.search = search;
    if (general.length) p.general = general.join(',');
    if (estado.length) p.estado = estado.join(',');
    if (inst.length) p.instalador = inst.join(',');
    if (prov.length) p.provincia = prov.join(',');
    if (prio.length) p.prioridad = prio.join(',');
    if (mats.length) p.materiales = mats.join(',');
    if (envios.length) p.envio = envios.join(',');
    if (soloActivos) p.activos = '1';
    return p;
  }, [search, general, estado, inst, prov, prio, mats, envios, soloActivos]);

  const load = React.useCallback(() => {
    API.get('/api/programacion/proyectos', params).then((r) => { setRows(r); setLoading(false); setUpdatedAt(new Date()); }).catch((e) => onToast(e.message, true));
  }, [params]);
  React.useEffect(() => { const t = setTimeout(load, 200); return () => clearTimeout(t); }, [load, tick]);
  React.useEffect(() => { API.get('/api/programacion/filtros').then(setFiltros).catch(() => {}); }, [tick]);
  usePgAutoRefresh(load);
  // GUT-260: con "Activos" y buscando, cuántas coincidencias quedan fuera por estar TERMINADO.
  const [terminadosOcultos, setTerminadosOcultos] = React.useState(0);
  React.useEffect(() => {
    if (!soloActivos || !search.trim()) { setTerminadosOcultos(0); return undefined; }
    const { activos, ...todos } = params;
    const t = setTimeout(() => API.get('/api/programacion/proyectos', todos)
      .then((r) => setTerminadosOcultos(r.filter((x) => x.estado_general === 'TERMINADO').length))
      .catch(() => setTerminadosOcultos(0)), 250);
    return () => clearTimeout(t);
  }, [params, tick]);

  const patch = async (id, body) => {
    try {
      const r = await API.patch(`/api/programacion/proyectos/${id}`, body);
      setRows((rs) => rs.map((x) => (x.id === id ? r : x)));
    } catch (e) { onToast(e.message || 'Error al guardar', true); load(); }
  };

  const sorted = React.useMemo(() => {
    const k = sort.k; const dir = sort.d === 'asc' ? 1 : -1;
    return [...rows].sort((a, b) => {
      const va = a[k]; const vb = b[k];
      if (va == null && vb == null) return 0; if (va == null) return 1; if (vb == null) return -1;
      if (k === 'numero') return (Number(va) - Number(vb)) * dir;
      if (k === 'materiales_estado') return (pgMatAvance(a) - pgMatAvance(b)) * dir; // INC-014: por % preparado
      return String(va).localeCompare(String(vb), 'es') * dir;
    });
  }, [rows, sort]);
  const th = (k, label, extra, title) => (
    <th className="sortable" onClick={() => setSort((s) => ({ k, d: s.k === k && s.d === 'asc' ? 'desc' : 'asc' }))} style={extra} title={title}>
      {label} <span className="sort">{sort.k === k ? (sort.d === 'asc' ? '↑' : '↓') : '↕'}</span>
    </th>
  );

  const hoy = pgToday(); const lunes = pgMonday(hoy); const viernes = pgAddDays(lunes, 4);
  const kpi = {
    activos: rows.filter((r) => r.estado_general !== 'TERMINADO').length,
    semana: rows.filter((r) => r.fecha_inicio && r.fecha_inicio <= viernes && r.fecha_fin >= lunes).length,
    sinFecha: rows.filter((r) => !r.fecha_inicio && (r.estado === 'INSTALACIÓN' || r.estado_general === 'REPASO')).length,
    bloqueados: rows.filter((r) => r.estado_general !== 'TERMINADO' && (!r.material_completo || !r.fabricacion_terminada)).length,
    repasos: rows.filter((r) => r.estado_general === 'REPASO').length,
  };
  const instOpts = pgPersonaOpts(instaladores);

  return (
    <>
      <div className="kpi-grid" style={{ gridTemplateColumns: 'repeat(5, 1fr)' }}>
        {[['Proyectos activos', kpi.activos, ''], ['Esta semana', kpi.semana, 'info'], ['Sin fecha (instalación / repaso)', kpi.sinFecha, 'warn'], ['Con material o fabricación pendiente', kpi.bloqueados, 'warn'], ['Repasos', kpi.repasos, 'err']].map(([l, n, c]) => (
          <div className="kpi" key={l}><div className="lbl">{l}</div><div className="row"><div className={`num ${c}`}>{n}</div></div></div>
        ))}
      </div>
      <div className="toolbar">
        <div className="tb-search"><Icon name="search" /><input placeholder="Nº, cliente, ubicación, descripción…" value={search} onChange={(e) => setSearch(e.target.value)} /></div>
        <div className="tb-divider" />
        <MultiChip label="General" options={PG_ESTADOS_GENERAL.map(([e]) => e)} selected={general} onChange={setGeneral} />
        <MultiChip label="Estado" options={PG_ESTADOS.map(([e]) => e)} selected={estado} onChange={setEstado} />
        <MultiChip icon="wrench" label="Instalador" options={instOpts} selected={inst} onChange={setInst} />
        <MultiChip icon="pin" label="Provincia" options={filtros.provincias || []} selected={prov} onChange={setProv} />
        <MultiChip label="Prioridad" options={PG_PRIORIDADES.map(([p]) => p)} selected={prio} onChange={setPrio} />
        <MultiChip label="Materiales" options={PG_MAT_FILTRO} selected={mats} onChange={setMats} />
        <MultiChip icon="truck" label="Transporte" options={pgEnvioFiltro(filtros.agencias)} selected={envios} onChange={setEnvios} />
        <div className="tb-spacer" />
        <span className="muted" style={{ fontSize: 11 }}>{updatedAt ? `Actualizado ${updatedAt.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })}` : ''}</span>
        <button className="icon-btn" title="Refrescar" onClick={load}><Icon name="bolt" size={13} /></button>
        <div className="seg">
          <button className={soloActivos ? 'on' : ''} onClick={() => setSoloActivos(true)}>Activos</button>
          <button className={!soloActivos ? 'on' : ''} onClick={() => setSoloActivos(false)}>Todos</button>
        </div>
        <button className="btn primary sm" onClick={() => setNuevo(true)}><Icon name="plus" /> Nuevo proyecto</button>
      </div>

      {soloActivos && terminadosOcultos > 0 && (
        <div className="pg-aviso-todos">
          <Icon name="search" size={13} />
          <span>Hay <strong>{terminadosOcultos}</strong> {terminadosOcultos === 1 ? 'coincidencia' : 'coincidencias'} en proyectos terminados</span>
          <button className="btn ghost sm" onClick={() => setSoloActivos(false)}>Ver todos</button>
        </div>
      )}
      <div className="tbl-wrap">
        <div className="tbl-scroll" style={{ maxHeight: 'calc(100vh - 330px)' }}>
          <table className="tbl pg-tbl pg-main">
            <thead>
              {/* GUT-248: table-layout fixed (erp.css). Todas las columnas llevan ancho salvo Observación,
                  que se queda con el espacio sobrante; General y Estado (GUT-249) caben enteros. */}
              <tr>
                {th('numero', 'Nº', { width: 84 })}
                {th('nombre', 'Proyecto', { width: 220 })}
                <th style={{ width: 150 }}>Descripción</th>
                <th style={{ width: 120 }}>Ubicación</th>
                {th('prioridad', 'Prioridad', { width: 95 })}
                {th('estado_general', 'General', { width: 100 })}
                {th('estado', 'Estado', { width: 130 })}
                {th('instalador_nombre', 'Instaladores', { width: 140 })}
                {th('fecha_inicio', 'Inicio', { width: 110 })}
                <th style={{ width: 55 }} title="Días asignados">Días</th>
                <th style={{ width: 55 }} title="Nº operarios">Op.</th>
                <th style={{ width: 75 }}>Fin</th>
                {th('fecha_prevista_cliente', 'F. cliente', { width: 110 })}
                {th('materiales_estado', 'Materiales', { width: 150 }, 'OK si todas las líneas de material del proyecto están en «Preparado» (o ya enviadas); si no, FALTA con lo que queda (lacado, corte, compra, por preparar) y los envíos parciales; — si no tiene materiales')}
                {th('envio', 'Envío', { width: 62 }, 'SÍ si el material se manda al cliente por agencia (ficha › Envío al cliente)')}
                <th style={{ width: 40 }} title="Material completo (marca manual)">Mat.</th>
                <th style={{ width: 40 }} title="Fabricación terminada">Fab.</th>
                <th>Observación</th>
                <th style={{ width: 36 }} />
              </tr>
            </thead>
            <tbody>
              {loading && <tr><td colSpan={19} style={{ textAlign: 'center', padding: 30, color: 'var(--ink-3)' }}>Cargando…</td></tr>}
              {!loading && sorted.length === 0 && <tr><td colSpan={19} style={{ textAlign: 'center', padding: 30, color: 'var(--ink-3)' }}>Sin proyectos</td></tr>}
              {!loading && sorted.map((r) => (
                <tr key={r.id} className="pg-row" onDoubleClick={() => onOpen(r.id)}>
                  <td className="num strong" onClick={() => onOpen(r.id)} style={{ cursor: 'pointer' }}>{r.numero}</td>
                  <td className="strong truncate" style={{ cursor: 'pointer' }} onClick={() => onOpen(r.id)} title={r.nombre}>
                    {r.nombre}{r.numero_expediente ? <span className="muted" style={{ fontSize: 10, marginLeft: 6 }}>Exp.</span> : null}
                  </td>
                  <td className="truncate"><PgEdit value={r.descripcion} onSave={(v) => patch(r.id, { descripcion: v })} title={r.descripcion || ''} /></td>
                  <td className="truncate"><PgEdit value={r.ubicacion} onSave={(v) => patch(r.id, { ubicacion: v })} title={r.ubicacion || ''} /></td>
                  <td><PgEdit type="select" className={`pg-pill ${PG_PRIORIDAD_CLS[r.prioridad] || ''}`} value={r.prioridad} options={PG_PRIORIDADES.map(([p]) => p)} onSave={(v) => patch(r.id, { prioridad: v })} /></td>
                  <td><PgEdit type="select" sinVacio className={`pg-pill ${PG_GENERAL_CLS[r.estado_general] || ''}`} value={r.estado_general} options={PG_ESTADOS_GENERAL.map(([e]) => e)} onSave={(v) => v && patch(r.id, pgCambioEstado(r, { estado_general: v }))} /></td>
                  <td><PgEdit type="select" className={`pg-pill ${PG_ESTADO_CLS[r.estado] || ''}`} value={r.estado} options={PG_ESTADOS.map(([e]) => e).concat(PG_ESTADO_CLS[r.estado] === undefined && r.estado ? [r.estado] : [])} onSave={(v) => patch(r.id, pgCambioEstado(r, { estado: v }))} /></td>
                  <td><PgPersonasPicker compact value={r.instalador_ids} instaladores={instaladores} onChange={(v) => patch(r.id, { instalador_ids: v })} style={r.instaladores?.[0]?.color ? { borderLeft: `3px solid ${r.instaladores[0].color}` } : undefined} /></td>
                  <td><PgEdit type="date" value={r.fecha_inicio} onSave={(v) => patch(r.id, { fecha_inicio: v })} /></td>
                  <td><PgEdit type="num" value={r.dias_asignados} onSave={(v) => patch(r.id, { dias_asignados: v })} /></td>
                  <td><PgEdit type="num" value={r.num_operarios} onSave={(v) => patch(r.id, { num_operarios: v })} /></td>
                  <td className="num muted">{pgFmt(r.fecha_fin)}</td>
                  {(() => { const av = pgAvisoCliente(r); return (
                    <td className={av ? `pg-fc ${av[0]}` : ''} title={av ? av[1] : (r.fecha_prevista_nota || '')}>
                      <PgEdit type="date" value={r.fecha_prevista_cliente} onSave={(v) => patch(r.id, { fecha_prevista_cliente: v })} />
                    </td>); })()}
                  <td><PgMatEstadoCell r={r} /></td>
                  <td title={r.envio ? [r.envio_agencia, r.envio_transportista, r.envio_tracking && `Tracking ${r.envio_tracking}`].filter(Boolean).join(' · ') || 'Con envío' : ''}>
                    <PgEdit type="select" className={r.envio ? 'pg-pill info' : ''} value={r.envio ? 'SI' : null} options={[['SI', 'SÍ']]} onSave={(v) => patch(r.id, { envio: v === 'SI' })} />
                  </td>
                  <td style={{ textAlign: 'center' }}><PgEdit type="check" value={r.material_completo} onSave={(v) => patch(r.id, { material_completo: v })} title="Material completo" /></td>
                  <td style={{ textAlign: 'center' }}><PgEdit type="check" value={r.fabricacion_terminada} onSave={(v) => patch(r.id, { fabricacion_terminada: v })} title="Fabricación terminada" /></td>
                  <td><PgEdit value={r.observacion} onSave={(v) => patch(r.id, { observacion: v })} title={r.observacion || ''} /></td>
                  <td><button className="icon-btn" title="Abrir ficha" onClick={() => onOpen(r.id)}><Icon name="chevron_right" size={13} /></button></td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>
      <div className="muted" style={{ fontSize: 11, marginTop: 8 }}>{sorted.length} proyectos · Fin = inicio + días laborables (sin fines de semana ni festivos). Doble clic o flecha para abrir la ficha.</div>
      {nuevo && <PgNuevoProyecto onClose={() => setNuevo(false)} onToast={onToast} onCreated={(p) => { setNuevo(false); refresh(); onOpen(p.id); }}
        onOpenExistente={(id) => { setNuevo(false); refresh(); onOpen(id); }} />}
    </>
  );
};

// ── Alta de proyecto (desde un expediente del ERP, desde un lead, o a mano) ──
// "Desde lead" (GUT-244): los leads antiguos tienen presupuesto y expediente fuera
// del ERP; Programación los planifica igual colgándolos del lead.
// GUT-260: caja "ya existe" con el proyecto que tiene ese nº (Abrir / Reabrir).
const PgExistenteBox = ({ p, onAbrir, onReabrir }) => (
  <div className="pg-existe">
    <div className="pg-existe-tit"><Icon name="info" size={14} /> Ya existe un proyecto con el nº {p.numero}</div>
    <div><span className="mono strong">{p.numero}</span> · {p.nombre}</div>
    {p.descripcion && <div className="muted" style={{ fontSize: 12 }}>{p.descripcion}</div>}
    <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
      <span className={`pg-pill ${PG_GENERAL_CLS[p.estado_general] || ''}`}>{p.estado_general}</span>
      {p.estado && <span className={`pg-pill ${PG_ESTADO_CLS[p.estado] || ''}`}>{p.estado}</span>}
    </div>
    {p.estado_general === 'TERMINADO' && (
      <div className="muted" style={{ fontSize: 11 }}>Está terminado: por eso no sale en la lista con el filtro "Activos" (sí con "Todos").</div>
    )}
    <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
      {p.estado_general === 'TERMINADO' && <button className="btn ghost sm" onClick={onReabrir}>Reabrir</button>}
      <button className="btn primary sm" onClick={onAbrir}>Abrir proyecto</button>
    </div>
  </div>
);
const PgYaEnProgramacion = ({ p }) => (
  <span className="pg-ya" title={`${p.numero} · ${p.nombre}${p.estado_general ? ` (${p.estado_general})` : ''}`}>
    ya en Programación{p.estado_general === 'TERMINADO' ? ' (terminado)' : ''} · abrir
  </span>
);

const PgNuevoProyecto = ({ onClose, onToast, onCreated, onOpenExistente }) => {
  const [modo, setModo] = React.useState('expediente');
  const [q, setQ] = React.useState('');
  const [exps, setExps] = React.useState([]);
  const [leads, setLeads] = React.useState([]);
  const [f, setF] = React.useState({ numero: '', nombre: '', descripcion: '', ubicacion: '', provincia: '', fecha_pedido: pgToday(), estado: 'MEDICIÓN' });
  const [busy, setBusy] = React.useState(false);
  const [existente, setExistente] = React.useState(null); // GUT-260
  // A mano: avisar en cuanto el nº escrito ya existe, antes de pulsar Crear.
  React.useEffect(() => {
    setExistente(null);
    if (modo !== 'manual' || !f.numero.trim()) return undefined;
    const t = setTimeout(() => API.get('/api/programacion/proyecto-existente', { numero: f.numero.trim() })
      .then(setExistente).catch(() => setExistente(null)), 300);
    return () => clearTimeout(t);
  }, [f.numero, modo]);
  // Abre el proyecto que ya existe. Si viene de un lead/expediente y el proyecto aún no
  // está enlazado (p. ej. importado del Excel), lo enlaza de paso.
  const abrirExistente = async (p, enlace) => {
    const body = {};
    if (enlace?.lead_id && !p.lead_id) body.lead_id = enlace.lead_id;
    if (enlace?.pedido_id && !p.pedido_id) body.pedido_id = enlace.pedido_id;
    if (Object.keys(body).length) await API.patch(`/api/programacion/proyectos/${p.id}`, body).catch(() => {});
    onOpenExistente(p.id);
  };
  const reabrir = async (p) => {
    if (!confirm(`El proyecto ${p.numero} está TERMINADO. ¿Reabrirlo? Su estado general pasará a EN CURSO y se abrirá la ficha.`)) return;
    try {
      await API.patch(`/api/programacion/proyectos/${p.id}`, { estado_general: 'EN CURSO' });
      onToast('Proyecto reabierto ✓');
      onOpenExistente(p.id);
    } catch (e) { onToast(e.message, true); }
  };
  React.useEffect(() => {
    if (modo === 'manual') return;
    const [url, set] = modo === 'lead' ? ['/api/programacion/leads-disponibles', setLeads] : ['/api/programacion/expedientes-disponibles', setExps];
    const t = setTimeout(() => API.get(url, q ? { search: q } : undefined).then(set).catch(() => set([])), 200);
    return () => clearTimeout(t);
  }, [q, modo]);
  const crear = async (body) => {
    setBusy(true);
    try { onCreated(await API.post('/api/programacion/proyectos', body)); onToast('Proyecto creado ✓'); }
    catch (e) {
      if (e.status === 409 && e.data?.existente) setExistente(e.data.existente); // GUT-260
      else onToast(e.message, true);
    } finally { setBusy(false); }
  };
  const desdeExp = (e) => (e.proyecto ? abrirExistente(e.proyecto, { pedido_id: e.id, lead_id: e.lead_id }) : crear({
    numero: String(e.numero_expediente).replace(/^C/i, ''), pedido_id: e.id, lead_id: e.lead_id || null, nombre: `${e.numero_expediente}. ${e.nombre_cliente || ''}`.trim(),
    descripcion: [e.producto_1, e.producto_2, e.producto_3, e.producto_4].filter(Boolean).join(' + ') || null,
    ubicacion: e.localidad || null, provincia: e.provincia ? e.provincia.toUpperCase() : null, fecha_pedido: e.fecha_pedido, estado: 'MEDICIÓN',
  }));
  const desdeLead = (l) => (l.proyecto ? abrirExistente(l.proyecto, { lead_id: l.id }) : crear({
    numero: l.numero_presupuesto, lead_id: l.id, nombre: `${l.numero_presupuesto}. ${l.nombre_cliente || ''}`.trim(),
    descripcion: l.productos_interes || l.descripcion || null,
    ubicacion: l.localidad || null, provincia: l.provincia ? l.provincia.toUpperCase() : null, fecha_pedido: l.fecha_solicitud, estado: f.estado,
  }));
  const set = (k) => (e) => setF((x) => ({ ...x, [k]: e.target.value }));
  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal-card" style={{ maxWidth: 560 }} onClick={(e) => e.stopPropagation()}>
        <header><h2>Nuevo proyecto</h2><button className="x" onClick={onClose}>×</button></header>
        <div className="seg" style={{ alignSelf: 'flex-start' }}>
          <button className={modo === 'expediente' ? 'on' : ''} onClick={() => setModo('expediente')}>Desde expediente</button>
          <button className={modo === 'lead' ? 'on' : ''} onClick={() => setModo('lead')}>Desde lead</button>
          <button className={modo === 'manual' ? 'on' : ''} onClick={() => setModo('manual')}>A mano</button>
        </div>
        {existente && <PgExistenteBox p={existente} onAbrir={() => abrirExistente(existente)} onReabrir={() => reabrir(existente)} />}
        {modo === 'lead' && (
          <>
            <div className="muted" style={{ fontSize: 12 }}>Para trabajos cuyo presupuesto y expediente no están en el ERP. El proyecto cuelga del lead y se enlazará solo al expediente si algún día se registra.</div>
            <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
              <input placeholder="Buscar lead por nº o cliente…" value={q} onChange={(e) => setQ(e.target.value)} autoFocus style={{ flex: 1 }} />
              <select value={f.estado} onChange={set('estado')}>{PG_ESTADOS.map(([e]) => <option key={e} value={e}>{e}</option>)}</select>
              <HelpTip text="Estado con el que se creará el proyecto (el general empieza siempre en EN CURSO). Un trabajo antiguo puede entrar ya en materiales o en instalación; elígelo antes de pinchar el lead. Luego se puede cambiar en la ficha." label="estado inicial" />
            </div>
            <div style={{ maxHeight: 300, overflow: 'auto', border: '1px solid var(--line)', borderRadius: 6 }}>
              {leads.length === 0 && <div className="muted" style={{ padding: 12, fontSize: 12 }}>Ningún lead{q ? ' con ese texto' : ''}.</div>}
              {leads.map((l) => (
                <div key={l.id} className={`pg-exp-row${l.proyecto ? ' ya' : ''}`} onClick={() => !busy && desdeLead(l)}>
                  <span className="mono strong">{l.numero_presupuesto}</span>
                  <span style={{ flex: 1 }}>{l.nombre_cliente || '—'}</span>
                  {l.proyecto ? <PgYaEnProgramacion p={l.proyecto} />
                    : <span className="muted" style={{ fontSize: 11 }}>{[l.estado, l.localidad, l.provincia].filter(Boolean).join(' · ')}</span>}
                </div>
              ))}
            </div>
            {leads.length >= 50 && <div className="muted" style={{ fontSize: 11 }}>Se muestran los 50 más recientes: busca por nº o cliente para encontrar uno más antiguo.</div>}
          </>
        )}
        {modo === 'expediente' && (
          <>
            <input placeholder="Buscar expediente por nº o cliente…" value={q} onChange={(e) => setQ(e.target.value)} autoFocus />
            <div style={{ maxHeight: 300, overflow: 'auto', border: '1px solid var(--line)', borderRadius: 6 }}>
              {exps.length === 0 && <div className="muted" style={{ padding: 12, fontSize: 12 }}>Ningún expediente{q ? ' con ese texto' : ''}.</div>}
              {exps.map((e) => (
                <div key={e.id} className={`pg-exp-row${e.proyecto ? ' ya' : ''}`} onClick={() => !busy && desdeExp(e)}>
                  <span className="mono strong">{e.numero_expediente}</span>
                  <span style={{ flex: 1 }}>{e.nombre_cliente || '—'}</span>
                  {e.proyecto ? <PgYaEnProgramacion p={e.proyecto} />
                    : <span className="muted" style={{ fontSize: 11 }}>{[e.localidad, e.provincia].filter(Boolean).join(', ')}</span>}
                </div>
              ))}
            </div>
            <div className="muted" style={{ fontSize: 12 }}>
              Aquí solo salen los expedientes registrados en el ERP. Para leads antiguos usa{' '}
              <a href="#" onClick={(ev) => { ev.preventDefault(); setModo('lead'); }}>'Desde lead'</a>.
            </div>
          </>
        )}
        {modo === 'manual' && (
          <>
            <div className="fields-grid">
              <label>Nº proyecto *<input value={f.numero} onChange={set('numero')} autoFocus /></label>
              <label>Fecha pedido<input type="date" value={f.fecha_pedido} onChange={set('fecha_pedido')} /></label>
              <label className="full">Nombre (cliente) *<input value={f.nombre} onChange={set('nombre')} placeholder="7648. FAUSTINO" /></label>
              <label className="full">Descripción<input value={f.descripcion} onChange={set('descripcion')} placeholder="CORTINAS DE CRISTAL + TECHO" /></label>
              <label>Ubicación<input value={f.ubicacion} onChange={set('ubicacion')} /></label>
              <label>Provincia<input value={f.provincia} onChange={set('provincia')} placeholder="SEVILLA" /></label>
            </div>
            <footer>
              <button className="btn ghost" onClick={onClose}>Cancelar</button>
              <button className="btn primary" disabled={busy || !f.numero || !f.nombre || !!existente} title={existente ? 'Ese nº ya existe: ábrelo desde el aviso de arriba' : undefined} onClick={() => crear({ ...f, provincia: f.provincia.toUpperCase() || null })}>Crear</button>
            </footer>
          </>
        )}
      </div>
    </div>
  );
};

// ═══════════════════════════════════════════════════════
// Ficha de proyecto (drawer)
// ═══════════════════════════════════════════════════════
// INC-002: campos de planificación del proyecto (los de Víctor). Se pintan en la pestaña
// «Planificación» de la ficha unificada y en la «Ficha» de los proyectos sin expediente.
const PgCamposProyecto = ({ d, patch, instaladores }) => {
  const F = PgField;
  return (
    <>
      <div className="section-title">Proyecto</div>
      <div className="fields-grid cols-3 pg-fields">
        <F label="Nº proyecto"><PgEdit value={d.numero} onSave={(v) => patch({ numero: v })} /></F>
        <F label="Fecha pedido"><PgEdit type="date" value={d.fecha_pedido} onSave={(v) => patch({ fecha_pedido: v })} /></F>
        <F label="RAL"><PgEdit value={d.ral} onSave={(v) => patch({ ral: v })} /></F>
        <F label="Nombre" full><PgEdit value={d.nombre} onSave={(v) => patch({ nombre: v })} /></F>
        <F label="Descripción" full><PgEdit value={d.descripcion} onSave={(v) => patch({ descripcion: v })} /></F>
        <F label="Ubicación"><PgEdit value={d.ubicacion} onSave={(v) => patch({ ubicacion: v })} /></F>
        <F label="Provincia"><PgEdit value={d.provincia} onSave={(v) => patch({ provincia: v ? v.toUpperCase() : null })} /></F>
        {/* INC-005: calle de la obra para el mapa. Vacía = la del envío («otra dirección») o la de la ficha del cliente; sin calle, el pueblo. */}
        <F label={<>Dirección obra <HelpTip text="Calle y número (y CP) de la obra, para situarla en Programación › Mapa. Si está vacía se usa la dirección de envío («otra dirección») o la de la ficha del cliente (si es de la misma provincia) y, si no hay calle, la ubicación + provincia." label="dirección de la obra" /></>}>
          <PgEdit value={d.obra_direccion} placeholder="Calle, nº, CP" onSave={(v) => patch({ obra_direccion: v })} />
          {d.geo_origen === 'fallido' && <div className="muted" style={{ fontSize: 11, marginTop: 3 }}>No se encontró la dirección en el mapa: colócalo a mano en Programación › Mapa.</div>}
          {d.geo_origen === 'nominatim' && d.geo_nivel === 'poblacion' && <div className="muted" style={{ fontSize: 11, marginTop: 3 }}>En el mapa está situado por la población (sin calle).</div>}
        </F>
        {/* INC-005: proceso que marca la oficina; color del marcador en Programación › Mapa. */}
        <F label={<>Proceso (mapa) <HelpTip text="Color del marcador en Programación › Mapa: pendiente de medida, pendiente revisión instalación, remate o garantía. Vacío = sin proceso (gris), salvo que el expediente tenga una reclamación o garantía abierta: entonces sale como garantía." label="proceso en el mapa" /></>}>
          <PgEdit type="select" value={d.proceso} options={PG_PROCESOS.map(([k, l]) => [k, l])} onSave={(v) => patch({ proceso: v })} />
        </F>
        <F label="Prioridad"><PgEdit type="select" className={`pg-pill ${PG_PRIORIDAD_CLS[d.prioridad] || ''}`} value={d.prioridad} options={PG_PRIORIDADES.map(([p]) => p)} onSave={(v) => patch({ prioridad: v })} /></F>
        <F label="Estado general"><PgEdit type="select" sinVacio className={`pg-pill ${PG_GENERAL_CLS[d.estado_general] || ''}`} value={d.estado_general} options={PG_ESTADOS_GENERAL.map(([e]) => e)} onSave={(v) => v && patch(pgCambioEstado(d, { estado_general: v }))} /></F>
        <F label="Estado"><PgEdit type="select" className={`pg-pill ${PG_ESTADO_CLS[d.estado] || ''}`} value={d.estado} options={PG_ESTADOS.map(([e]) => e)} onSave={(v) => patch(pgCambioEstado(d, { estado: v }))} /></F>
        <F label="Medidor">
          {/* GUT-259: personas de Programación › Configuración. El texto de antes queda de respaldo. */}
          <PgPersonasPicker compact value={d.medidor_ids} instaladores={instaladores} placeholder="Sin medidor" vacio="— Sin medidor —" onChange={(v) => patch({ medidor_ids: v })} />
          {!(d.medidor_ids || []).length && d.medidor && <div className="muted" style={{ fontSize: 11, marginTop: 3 }} title="Lo que se escribió a mano antes de elegir personas. Elige arriba quién midió.">Texto antiguo: {d.medidor}</div>}
        </F>
        <F label="Fecha medición"><PgEdit type="date" value={d.fecha_medicion} onSave={(v) => patch({ fecha_medicion: v })} /></F>
        <F label="Operario fábrica"><PgEdit value={d.operario_fabrica} onSave={(v) => patch({ operario_fabrica: v })} /></F>
        <F label="Fecha fabricación"><PgEdit type="date" value={d.fecha_fabricacion} onSave={(v) => patch({ fecha_fabricacion: v })} /></F>
        <F label="Observación" full><PgEdit type="textarea" value={d.observacion} onSave={(v) => patch({ observacion: v })} /></F>
      </div>
      <div className="section-title" style={{ marginTop: 18 }}>Instalación</div>
      <div className="fields-grid cols-3 pg-fields">
        <F label="Instaladores"><PgPersonasPicker compact value={d.instalador_ids} instaladores={instaladores} onChange={(v) => patch({ instalador_ids: v })} /></F>
        <F label="Fecha inicio"><PgEdit type="date" value={d.fecha_inicio} onSave={(v) => patch({ fecha_inicio: v })} /></F>
        <F label={<>F. prevista cliente <HelpTip text="Fecha tentativa que pidió el cliente. Es un compromiso, no una asignación: sale punteada en el calendario, sin instalador, y no cuenta en Carga." label="fecha prevista por el cliente" /></>}>
          <div className="row-flex" style={{ gap: 6, alignItems: 'center' }}>
            <PgEdit type="date" value={d.fecha_prevista_cliente} onSave={(v) => patch({ fecha_prevista_cliente: v })} />
            {d.fecha_prevista_cliente && !d.fecha_inicio && <button className="btn ghost sm" title="Planificar la instalación en la fecha que pidió el cliente" onClick={() => patch({ fecha_inicio: d.fecha_prevista_cliente })}>Usar como inicio</button>}
          </div>
        </F>
        <F label="Nota fecha cliente"><PgEdit value={d.fecha_prevista_nota} onSave={(v) => patch({ fecha_prevista_nota: v })} placeholder="p. ej. quiere antes de vacaciones" /></F>
        <F label="Fecha fin (calculada)"><div className="v">{pgFmtLong(d.fecha_fin)}</div></F>
        <F label="Días asignados"><PgEdit type="num" value={d.dias_asignados} onSave={(v) => patch({ dias_asignados: v })} /></F>
        <F label="Nº operarios"><PgEdit type="num" value={d.num_operarios} onSave={(v) => patch({ num_operarios: v })} /></F>
        <F label="Vehículo"><PgEdit value={d.vehiculo} onSave={(v) => patch({ vehiculo: v })} /></F>
        <F label="Fecha instalación real"><PgEdit type="date" value={d.fecha_instalacion_real} onSave={(v) => patch({ fecha_instalacion_real: v })} /></F>
        <F label="Material completo"><span className="pg-check-lbl"><PgEdit type="check" value={d.material_completo} onSave={(v) => patch({ material_completo: v })} /> {d.material_completo ? 'Sí' : 'No'}</span></F>
        <F label="Fabricación terminada"><span className="pg-check-lbl"><PgEdit type="check" value={d.fabricacion_terminada} onSave={(v) => patch({ fabricacion_terminada: v })} /> {d.fabricacion_terminada ? 'Sí' : 'No'}</span></F>
        <F label="Notas taller / preparación" full><PgEdit type="textarea" value={d.notas_taller} onSave={(v) => patch({ notas_taller: v })} /></F>
      </div>
      <div className="muted" style={{ fontSize: 11, marginTop: 14 }}>
        Última modificación: {d.updated_at ? new Date(d.updated_at).toLocaleString('es-ES') : '—'}{d.updated_by ? ` · ${d.updated_by}` : ''}
      </div>
    </>
  );
};

// INC-019 (Juanjo): bloque «Envío al cliente». Se guarda en el proyecto de Programación;
// sale en la ficha del expediente (Info) —también en la del proyecto, que es la misma— y
// en la ficha de los proyectos sin expediente. La dirección se elige como en una factura:
// la de la ficha del cliente, la de la obra u «otra dirección» (dirección, CP, teléfono).
// El albarán de entrega impreso la saca a la derecha con la agencia y el transportista.
const EnvioClientePanel = ({ proyectoId, pedidoId, onToast }) => {
  const [e, setE] = React.useState(null);
  const [sug, setSug] = React.useState({ agencias: [], transportistas: [] });
  const toast = (m, err) => onToast && onToast(m, err);
  const load = React.useCallback(() => {
    const q = proyectoId ? { proyecto_id: proyectoId } : { pedido_id: pedidoId };
    API.get('/api/programacion/envio', q).then(setE).catch((err) => setE({ error: err.message }));
  }, [proyectoId, pedidoId]);
  React.useEffect(() => { load(); }, [load]);
  React.useEffect(() => { API.get('/api/programacion/filtros').then((f) => setSug({ agencias: f.agencias || [], transportistas: f.transportistas || [] })).catch(() => {}); }, []);
  if (!e) return null;
  const titulo = <div className="section-title" style={{ marginTop: 18 }}><Icon name="truck" size={13} /> Envío al cliente</div>;
  if (e.error) return <>{titulo}<div className="muted" style={{ fontSize: 12 }}>{e.error}</div></>;
  if (!e.proyecto) {
    return <>{titulo}<div className="muted" style={{ fontSize: 12.5, marginBottom: 8 }}>Este expediente aún no está en Programación. El envío se indica en su proyecto: créalo en Programación › Nuevo proyecto › Desde expediente.</div></>;
  }
  const patch = async (body) => {
    try { await API.patch(`/api/programacion/proyectos/${e.proyecto.id}`, body); load(); }
    catch (err) { toast(err.message || 'Error al guardar', true); }
  };
  const op = e.opciones || {};
  // Al marcar el envío, por defecto la dirección del cliente (si la tiene; si no, la de la obra).
  const activar = (v) => patch(v && !e.envio_direccion_tipo
    ? { envio: true, envio_direccion_tipo: op.cliente?.direccion ? 'cliente' : op.obra?.direccion ? 'obra' : 'otra' }
    : { envio: v });
  const etq = (txt, dir) => `${txt} — ${dir || 'sin dirección en la ficha'}`;
  const F = PgField;
  return (
    <>
      {titulo}
      <label className="pg-check-lbl" style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8, fontSize: 13 }}>
        <PgEdit type="check" value={e.envio} onSave={activar} /> Se envía al cliente por agencia de transporte
      </label>
      {e.envio && (
        <div className="fields-grid cols-3 pg-fields">
          <F label={<>Dirección de envío <HelpTip text="Como en la factura: la dirección de la ficha del cliente, la de la obra (ubicación del proyecto) u otra escrita a mano. Sale en el albarán de entrega." label="dirección de envío" /></>} full>
            <PgEdit type="select" sinVacio value={e.envio_direccion_tipo || 'otra'} onSave={(v) => patch({ envio_direccion_tipo: v })}
              options={[['cliente', etq('Dirección del cliente', op.cliente?.direccion)], ['obra', etq('Dirección de la obra', op.obra?.direccion)], ['otra', 'Otra dirección']]} />
          </F>
          {e.envio_direccion_tipo === 'otra' ? (
            <>
              <F label="Dirección" full><PgEdit value={e.envio_direccion} onSave={(v) => patch({ envio_direccion: v })} placeholder="Calle, nº, localidad, provincia" /></F>
              <F label="CP"><PgEdit value={e.envio_cp} onSave={(v) => patch({ envio_cp: v })} placeholder="41000" /></F>
              <F label="Teléfono"><PgEdit value={e.envio_telefono} onSave={(v) => patch({ envio_telefono: v })} placeholder="Contacto en destino" /></F>
            </>
          ) : (
            <F label="Teléfono"><div className="v">{e.destino?.telefono || <span className="muted">— (sin teléfono en la ficha del cliente)</span>}</div></F>
          )}
          <F label="Agencia"><PgEdit value={e.envio_agencia} onSave={(v) => patch({ envio_agencia: v })} placeholder="p. ej. NACEX" list="envio-agencias" /></F>
          <F label="Nº tracking"><PgEdit value={e.envio_tracking} onSave={(v) => patch({ envio_tracking: v })} placeholder="Nº de seguimiento" /></F>
          <F label="Transportista"><PgEdit value={e.envio_transportista} onSave={(v) => patch({ envio_transportista: v })} placeholder="Quién lo lleva" list="envio-transportistas" /></F>
          <datalist id="envio-agencias">{sug.agencias.map((a) => <option key={a} value={a} />)}</datalist>
          <datalist id="envio-transportistas">{sug.transportistas.map((a) => <option key={a} value={a} />)}</datalist>
        </div>
      )}
    </>
  );
};
window.EnvioClientePanel = EnvioClientePanel;

// INC-002: proyecto sin expediente → buscar el expediente del ERP y enlazarlo. Al enlazar
// (PATCH pedido_id) el trigger trg_proyecto_enlaza_bom pasa sus materiales al expediente y
// la ficha pasa a ser la del expediente + pestañas de Programación.
const PgEnlazarExpediente = ({ d, onEnlazar }) => {
  const [abierto, setAbierto] = React.useState(false);
  const [q, setQ] = React.useState('');
  const [rows, setRows] = React.useState(null);
  React.useEffect(() => {
    if (!abierto) return undefined;
    const t = setTimeout(() => API.get('/api/programacion/expedientes-disponibles', q ? { search: q } : undefined).then(setRows).catch(() => setRows([])), 250);
    return () => clearTimeout(t);
  }, [abierto, q]);
  const enlazar = (e) => {
    if (!confirm(`¿Enlazar el proyecto ${d.numero} con el expediente ${e.numero_expediente}${e.nombre_cliente ? ` (${e.nombre_cliente})` : ''}?\n\nLa ficha pasará a ser la del expediente, con las tareas y la planificación del proyecto. Sus materiales pasan también al expediente.`)) return;
    onEnlazar(e.id);
  };
  return (
    <div className="pg-note" style={{ marginBottom: 12 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
        <Icon name="info" size={14} />
        <span style={{ flex: 1, minWidth: 200 }}>
          Este proyecto aún no tiene expediente en el ERP
          {d.lead_id ? ` (cuelga del lead ${d.lead_numero || d.lead_id}; si se registra su expediente se enlazará solo)` : ''}.
          {' '}Enlázalo para ver la ficha completa, igual que la de Expedientes.
        </span>
        <button className="btn ghost sm" onClick={() => setAbierto((x) => !x)}><Icon name="package" size={12} /> {abierto ? 'Cancelar' : 'Enlazar con expediente'}</button>
      </div>
      {abierto && (
        <div style={{ marginTop: 10 }}>
          <div className="tb-search" style={{ maxWidth: 360 }}><Icon name="search" /><input autoFocus placeholder="Nº expediente o cliente…" value={q} onChange={(e) => setQ(e.target.value)} /></div>
          <div style={{ maxHeight: 240, overflowY: 'auto', marginTop: 6 }}>
            {rows === null && <div className="muted" style={{ fontSize: 12 }}>Buscando…</div>}
            {rows && rows.length === 0 && <div className="muted" style={{ fontSize: 12 }}>Sin expedientes</div>}
            {rows && rows.map((e) => {
              const otro = e.proyecto && e.proyecto.id !== d.id && e.proyecto.pedido_id === e.id; // solo bloquea si ya está enlazado a otro
              return (
                <div key={e.id} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0', borderTop: '1px solid var(--line)', fontSize: 12.5 }}>
                  <span className="mono strong" style={{ width: 90 }}>{e.numero_expediente}</span>
                  <span className="truncate" style={{ flex: 1 }}>{e.nombre_cliente || '—'}{e.localidad ? ` · ${e.localidad}` : ''}</span>
                  {otro
                    ? <span className="muted" style={{ fontSize: 11 }}>ya es el proyecto {e.proyecto.numero}</span>
                    : <button className="btn primary sm" onClick={() => enlazar(e)}>Enlazar</button>}
                </div>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
};

// INC-002 (Juanjo, 16 oct): «cuando abres la hoja del proyecto, que sea igual que la de
// expediente, pero además con el índice de tareas para asignarlas al operario».
// Con expediente enlazado (y permiso para verlo) la ficha ES ExpedienteDrawer (mismos
// componentes y datos: lee y escribe en ese expediente) más las pestañas de Programación:
// Tareas, Planificación, Fechas y Portes. Sin expediente, la ficha de siempre con la
// opción de enlazarlo.
const PgProyectoDrawer = ({ id, initialTab, onClose, onToast, instaladores, onChanged }) => {
  const [d, setD] = React.useState(null);
  const [tab, setTab] = React.useState(initialTab || 'info');
  // INC-011: pestaña Costes solo para quien tiene Compras u Órdenes.
  const verCostes = !!(window.puedeVerCostes && window.puedeVerCostes());
  const load = React.useCallback(() => API.get(`/api/programacion/proyectos/${id}`).then(setD).catch((e) => onToast(e.message, true)), [id]);
  React.useEffect(() => { load(); }, [load]);
  const patch = async (body) => {
    try { const r = await API.patch(`/api/programacion/proyectos/${id}`, body); setD((x) => ({ ...x, ...r })); onChanged(); }
    catch (e) { onToast(e.message, true); }
  };
  const borrar = async () => {
    if (!confirm(`¿Eliminar el proyecto ${d.numero} de la programación? Se borran también sus fechas, materiales y tareas.`)) return;
    try { await API.del(`/api/programacion/proyectos/${id}`); onToast('Proyecto eliminado'); onChanged(); onClose(); } catch (e) { onToast(e.message, true); }
  };
  const enlazarExpediente = async (pedidoId) => {
    try {
      await API.patch(`/api/programacion/proyectos/${id}`, { pedido_id: pedidoId });
      onToast('Proyecto enlazado con el expediente ✓'); await load(); onChanged();
    } catch (e) { onToast(e.message, true); }
  };
  const abrirLead = () => window.dispatchEvent(new CustomEvent('erp:open-lead', { detail: { id: d.lead_id } }));
  // Ancho: por defecto amplio (las tablas de materiales/portes no caben en 760px); botón para ocupar toda la pantalla.
  const [full, setFull] = React.useState(() => { try { return localStorage.getItem('pg_drawer_full') === '1'; } catch { return false; } });
  const toggleFull = () => setFull((x) => { try { localStorage.setItem('pg_drawer_full', x ? '0' : '1'); } catch {} return !x; });
  const recargar = () => { load(); onChanged(); };

  // Pestañas propias de Programación, compartidas por las dos fichas.
  const pgTabs = d ? {
    tareas: { label: `Tareas${d.tareas.length ? ` (${d.tareas.length})` : ''}`, render: () => <PgTareasLista rows={d.tareas} proyectoId={d.id} instaladores={instaladores} reload={recargar} onToast={onToast} /> },
    fechas: { label: `Fechas${d.eventos.length ? ` (${d.eventos.length})` : ''}`, render: () => <PgEventos d={d} reload={recargar} onToast={onToast} instaladores={instaladores} /> },
    portes: { label: `Portes${d.portes.length ? ` (${d.portes.length})` : ''}`, render: () => <PgPortesLista rows={d.portes} proyectoId={d.id} instaladores={instaladores} reload={recargar} onToast={onToast} compact /> },
  } : {};
  const avisoCliente = () => { const av = d && pgAvisoCliente(d); return av ? <div className={`pg-note pg-fc-note ${av[0]}`}>{av[1]}.</div> : null; };

  // Ficha unificada: la del expediente + pestañas de Programación.
  const puedeExp = !!(window.hojaPermisos && window.hojaPermisos().expediente);
  if (d && d.pedido_id && puedeExp && window.ExpedienteDrawer) {
    const extraTabs = [
      { key: 'tareas', ...pgTabs.tareas },
      { key: 'planificacion', label: 'Planificación', render: () => <>{avisoCliente()}<PgCamposProyecto d={d} patch={patch} instaladores={instaladores} /></> },
      { key: 'fechas', ...pgTabs.fechas },
      { key: 'portes', ...pgTabs.portes },
    ];
    const headExtra = (
      <>
        <PgPill v={d.estado_general} map={PG_GENERAL_CLS} />
        {d.estado && <PgPill v={d.estado} map={PG_ESTADO_CLS} />}
        <button className="icon-btn" onClick={borrar} title="Eliminar el proyecto de la programación (el expediente no se toca)"><Icon name="trash" /></button>
      </>
    );
    return (
      <window.ExpedienteDrawer
        pedidoId={d.pedido_id}
        initialTab={initialTab || 'info'}
        onClose={onClose}
        onToast={onToast}
        onRefresh={recargar}
        proyecto={{ numero: d.numero, nombre: d.nombre }}
        extraTabs={extraTabs}
        headExtra={headExtra}
        className="pg-drawer"
      />
    );
  }

  // Sin expediente (o sin permiso para verlo): la ficha de Programación de siempre.
  const TABS = [['info', 'Ficha'], ['fechas'], ['materiales', `Materiales${d && d.materiales.length ? ` (${d.materiales.length})` : ''}`], ['tareas'], ['portes'], ...(verCostes ? [['costes', 'Costes']] : [])];
  return (
    <>
      <div className="drawer-bg open" onClick={onClose} />
      <div className={`drawer open pg-drawer ${full ? 'pg-drawer-full' : ''}`}>
        <div className="dr-head">
          <span className="id mono">{d ? d.numero : '…'}</span>
          <h2 style={{ flex: 1, minWidth: 0 }} title={d ? d.nombre : ''}>{d ? d.nombre : ''}</h2>
          {d && <PgPill v={d.estado_general} map={PG_GENERAL_CLS} />}
          {d && d.estado && <PgPill v={d.estado} map={PG_ESTADO_CLS} />}
          <div className="actions">
            {d && d.materiales.length > 0 && <button className="btn ghost sm" onClick={() => window.abrirAlbaran({ proyectoId: d.id, onSaved: recargar })} title="Albarán de entrega: el material que sale hacia la obra con el instalador"><Icon name="truck" size={12} /> Albarán</button>}
            {d && d.pedido_id && <button className="btn ghost sm" onClick={() => window.dispatchEvent(new CustomEvent('erp:open-expediente', { detail: { pedidoId: d.pedido_id } }))}><Icon name="package" size={12} /> Expediente</button>}
            {d && !d.pedido_id && d.lead_id && <button className="btn ghost sm" onClick={abrirLead} title="Sin expediente en el ERP: el proyecto cuelga del lead"><Icon name="leads" size={12} /> Lead {d.lead_numero || ''}</button>}
            <button className="icon-btn" onClick={toggleFull} title={full ? 'Reducir panel' : 'Ampliar a toda la pantalla'}><Icon name={full ? 'minimize' : 'maximize'} /></button>
            <button className="icon-btn" onClick={borrar} title="Eliminar"><Icon name="trash" /></button>
            <button className="icon-btn" onClick={onClose}><Icon name="x" /></button>
          </div>
        </div>
        <div className="dr-tabs">
          {TABS.map(([k, l]) => <button key={k} className={tab === k ? 'on' : ''} onClick={() => setTab(k)}>{l || (pgTabs[k] ? pgTabs[k].label : k[0].toUpperCase() + k.slice(1))}</button>)}
        </div>
        <div className="dr-body">
          {!d && <div className="muted">Cargando…</div>}
          {d && tab === 'info' && (
            <>
              {!d.pedido_id && <PgEnlazarExpediente d={d} onEnlazar={enlazarExpediente} />}
              {avisoCliente()}
              <PgCamposProyecto d={d} patch={patch} instaladores={instaladores} />
              <EnvioClientePanel proyectoId={d.id} onToast={onToast} />
            </>
          )}
          {d && pgTabs[tab] && pgTabs[tab].render()}
          {d && tab === 'materiales' && <PgMaterialesLista rows={d.materiales} proyectoId={d.id} reload={recargar} onToast={onToast} />}
          {d && d.id && tab === 'costes' && verCostes && window.CostesPanel && <window.CostesPanel proyectoId={d.id} onToast={onToast} />}
        </div>
      </div>
    </>
  );
};

// ── Fechas extra del proyecto (medición, repaso, portes, segundas visitas) ──
const PgEventos = ({ d, reload, onToast, instaladores }) => {
  const [f, setF] = React.useState({ tipo: 'medicion', fecha_inicio: pgToday(), fecha_fin: '', instalador_ids: ((d.medidor_ids || []).length ? d.medidor_ids : d.instalador_ids) || [], num_operarios: '', nota: '' }); // GUT-259: la medición la hace el medidor
  const add = async () => {
    try {
      await API.post(`/api/programacion/proyectos/${d.id}/eventos`, { ...f, fecha_fin: f.fecha_fin || f.fecha_inicio });
      setF((x) => ({ ...x, nota: '', fecha_fin: '' })); reload(); onToast('Fecha añadida ✓');
    } catch (e) { onToast(e.message, true); }
  };
  const patch = (id, body) => API.patch(`/api/programacion/eventos/${id}`, body).then(reload).catch((e) => onToast(e.message, true));
  const del = (id) => confirm('¿Quitar esta fecha?') && API.del(`/api/programacion/eventos/${id}`).then(reload).catch((e) => onToast(e.message, true));
  const set = (k) => (e) => setF((x) => ({ ...x, [k]: e.target.value }));
  return (
    <>
      {d.fecha_inicio && (
        <div className="pg-note">Instalación principal: <strong>{pgFmtLong(d.fecha_inicio)} → {pgFmtLong(d.fecha_fin)}</strong>{d.instalador_nombre ? ` · ${d.instalador_nombre}` : ''} (se edita en la ficha).</div>
      )}
      <div className="pg-scroll-x"><table className="tbl tbl-compact pg-tbl" style={{ marginTop: 10 }}>
        <thead><tr><th>Tipo</th><th>Inicio</th><th>Fin</th><th style={{ minWidth: 160 }}>Instaladores</th><th style={{ width: 50 }}>Op.</th><th style={{ minWidth: 220 }}>Nota</th><th>Estado</th><th /></tr></thead>
        <tbody>
          {d.eventos.length === 0 && <tr><td colSpan={8} className="muted" style={{ textAlign: 'center' }}>Sin fechas adicionales</td></tr>}
          {d.eventos.map((e) => (
            <tr key={e.id} className="pg-row">
              <td><PgEdit type="select" value={e.tipo} options={Object.entries(PG_TIPOS_EVENTO)} onSave={(v) => v && patch(e.id, { tipo: v })} /></td>
              <td><PgEdit type="date" value={e.fecha_inicio} onSave={(v) => v && patch(e.id, { fecha_inicio: v })} /></td>
              <td><PgEdit type="date" value={e.fecha_fin} onSave={(v) => v && patch(e.id, { fecha_fin: v })} /></td>
              <td><PgPersonasPicker compact value={e.instalador_ids} instaladores={instaladores} onChange={(v) => patch(e.id, { instalador_ids: v })} /></td>
              <td><PgEdit type="num" value={e.num_operarios} onSave={(v) => patch(e.id, { num_operarios: v })} /></td>
              <td className="pg-td-text"><PgEdit multiline value={e.nota} onSave={(v) => patch(e.id, { nota: v })} placeholder="Nota" /></td>
              <td><PgEdit type="select" value={e.estado} options={[['programado', 'Programado'], ['hecho', 'Hecho'], ['ko', 'KO']]} onSave={(v) => v && patch(e.id, { estado: v })} /></td>
              <td><button className="icon-btn" onClick={() => del(e.id)}><Icon name="trash" size={12} /></button></td>
            </tr>
          ))}
        </tbody>
      </table></div>
      <div className="section-title" style={{ marginTop: 18 }}>Añadir fecha</div>
      <div className="pg-addrow">
        <select value={f.tipo} onChange={set('tipo')}>{Object.entries(PG_TIPOS_EVENTO).map(([k, l]) => <option key={k} value={k}>{l}</option>)}</select>
        <input type="date" value={f.fecha_inicio} onChange={set('fecha_inicio')} />
        <input type="date" value={f.fecha_fin} onChange={set('fecha_fin')} title="Fin (vacío = mismo día)" />
        <PgPersonasPicker value={f.instalador_ids} instaladores={instaladores} onChange={(v) => setF((x) => ({ ...x, instalador_ids: v }))} style={{ width: 200 }} />
        <input type="number" placeholder="Op." value={f.num_operarios} onChange={set('num_operarios')} style={{ width: 60 }} />
        <input placeholder="Nota" value={f.nota} onChange={set('nota')} style={{ flex: 1 }} />
        <button className="btn primary sm" onClick={add} disabled={!f.fecha_inicio}><Icon name="plus" /> Añadir</button>
      </div>
    </>
  );
};

// INC-001: el artículo del «Añadir material» se elige con MatArticuloPicker
// (articulo-catalogo.jsx): obligatorio, sin familias, con «Crear artículo en Artículos»
// o, si aún no existe, la línea va como comentario (sin artículo, pendiente de crearlo).
// Proveedor habitual de un artículo: el marcado por defecto o, si solo tiene uno, ese.
const pgProvArticulo = (m) => m.proveedor_default_id || ((m.proveedores || []).length === 1 ? m.proveedores[0].proveedor_id : null);

// GUT-273: prioridad de los materiales = urgencia del proyecto (fecha de instalación o, si no
// hay, la prevista del cliente). Urgente: faltan ≤7 días (o ya pasó) y el material aún no
// está en fábrica (ni recibido, ni preparado, ni enviado).
const PG_MAT_URGENTE_DIAS = 7;
const pgMatUrgente = (m, hoy) => !!m.fecha_urgencia && m.fecha_urgencia <= pgAddDays(hoy, PG_MAT_URGENTE_DIAS)
  && !['recibido', 'preparado', 'enviado'].includes(m.estado_material);
const PgUrgenciaCell = ({ m, urgente, hoy }) => {
  if (!m.fecha_urgencia) return <span className="muted">—</span>;
  const prevista = m.fecha_urgencia_tipo === 'prevista';
  const pasada = m.fecha_urgencia < hoy;
  const tip = `${prevista ? 'Fecha prevista que pidió el cliente (sin instalación planificada)' : 'Fecha de instalación planificada'}${urgente ? (pasada ? ' · ya pasó y el material sigue pendiente' : ` · faltan ${PG_MAT_URGENTE_DIAS} días o menos y el material sigue pendiente`) : ''}`;
  return (
    <div title={tip} style={{ whiteSpace: 'nowrap', lineHeight: 1.3 }}>
      <div className={urgente ? `pg-urg-fecha ${pasada ? 'err' : 'warn'}` : ''}>{pgFmt(m.fecha_urgencia)}</div>
      <div className="muted" style={{ fontSize: 11 }}>{prevista ? 'Prevista cliente' : 'Instalación'}</div>
    </div>
  );
};

// ── Listas reutilizables (drawer + pestañas globales) ──
// GUT-252: las líneas son las mismas de Expediente › Materiales (bom_lineas). El proveedor
// se elige de la ficha de proveedores; el texto suelto del Excel que no casó sale con ⚠.
// El estado no se edita a mano: la compra la mueve la orden a proveedor; procesos (lacado,
// acción) y "preparado" se gestionan desde la píldora de estado (MaterialEstado).
const PgMaterialesLista = ({ rows, proyectoId, proyectos, reload, onToast, showProyecto, onOpen }) => {
  const provItems = window.useMatProveedores();
  const SIN_ARTICULO = { material_id: '', codigo: '', unidad: '', descripcion: '' };
  const [f, setF] = React.useState({ proyecto_id: proyectoId || '', largo: '', ancho: '', alto: '', cantidad: '', proveedor_id: '', observacion: '', de_stock: false, ...SIN_ARTICULO });
  const [art, setArt] = React.useState(null); // artículo del catálogo enlazado en el alta
  const [comentario, setComentario] = React.useState(false); // INC-001: línea sin artículo
  const set = (k) => (e) => setF((x) => ({ ...x, [k]: e.target.value }));
  const pasarAComentario = (texto) => { setArt(null); setComentario(true); setF((x) => ({ ...x, ...SIN_ARTICULO, descripcion: texto || '' })); };
  const volverAlCatalogo = () => { setComentario(false); setF((x) => ({ ...x, ...SIN_ARTICULO })); };
  const elegirArticulo = (m) => {
    setComentario(false);
    if (!m) { quitarArticulo(); return; }
    setArt(m);
    const prov = pgProvArticulo(m);
    setF((x) => ({ ...x, descripcion: m.nombre, material_id: m.id, codigo: m.referencia || '', unidad: m.unidad_medida || '', proveedor_id: prov ? String(prov) : '' }));
  };
  const quitarArticulo = () => { setArt(null); setF((x) => ({ ...x, ...SIN_ARTICULO })); };
  const postMaterial = (body) => (proyectoId ? API.post(`/api/programacion/proyectos/${proyectoId}/materiales`, body) : API.post('/api/programacion/materiales', body));
  const limpiarAlta = () => { setF((x) => ({ ...x, largo: '', ancho: '', alto: '', cantidad: '', proveedor_id: '', observacion: '', ...SIN_ARTICULO })); setArt(null); setComentario(false); };
  // GUT-272: un artículo kit se despliega en sus componentes (vista previa antes de crear).
  const [verKit, setVerKit] = React.useState(false);
  const esKit = !!(art && art.kit_componentes > 0);
  const crearKit = async (lineas, desp) => {
    try {
      const origen = kitCampos(desp); // INC-009: todas las líneas en el mismo grupo «KIT …»
      for (const l of lineas) {
        await postMaterial({
          ...f, ...origen, descripcion: l.nombre, material_id: l.material_id, codigo: l.referencia || '', unidad: l.unidad || '',
          cantidad: l.cantidad, proveedor_id: f.proveedor_id || l.proveedor_id || '', observacion: kitObservacion(desp, l, f.observacion),
        });
      }
      limpiarAlta(); reload(); onToast(`Kit desplegado: ${lineas.length} línea${lineas.length === 1 ? '' : 's'} ✓`);
    } catch (e) { reload(); onToast(e.message, true); throw e; }
  };
  const add = async () => {
    if (esKit) { setVerKit(true); return; }
    if (comentario ? !f.descripcion.trim() : !f.material_id) {
      onToast(comentario ? 'Escribe qué material es (va como comentario hasta crear el artículo)' : 'Elige el artículo del catálogo (si no está, créalo en Artículos o añádelo como comentario).', true);
      return;
    }
    try {
      await postMaterial(comentario ? { ...f, material_id: null, sin_articulo: true } : f);
      limpiarAlta(); reload(); onToast(comentario ? 'Material añadido como comentario (sin artículo) ✓' : 'Material añadido ✓');
    } catch (e) { onToast(e.message, true); }
  };
  // GUT-272: tabla compartida con Expediente › Materiales (materiales-tabla.jsx). Todo se
  // cambia con «Editar»; la tabla manda nombres de bom_lineas y aquí van a la API de Programación.
  const guardar = (m, { nombre, ...body }) => API.patch(`/api/programacion/materiales/${m.id}`, nombre === undefined ? body : { ...body, descripcion: nombre });
  const borrar = (m) => API.del(`/api/programacion/materiales/${m.id}`);
  const patch = (id, body) => API.patch(`/api/programacion/materiales/${id}`, body).then(reload).catch((e) => onToast(e.message, true));
  const hoy = pgToday();
  const extraCols = showProyecto ? [
    { key: 'proyecto', th: 'Proyecto', thProps: { style: { width: 220 } }, tdProps: { style: { maxWidth: 220 } },
      td: (m) => <MatClienteCell m={m} onOpen={onOpen} picker={<PgProyectoPicker compact value={m.proyecto_id} proyectos={proyectos} onChange={(v) => patch(m.id, { proyecto_id: v })} />} />,
      tdKit: (ls) => <MatClienteCell m={ls[0]} onOpen={onOpen} /> }, // INC-009: cabecera del kit (sin mover: cada línea se mueve sola)
    { key: 'instalacion', th: 'Instalación ↑',
      thProps: { style: { width: 104 }, title: 'Fecha de instalación del proyecto (o la prevista del cliente si aún no hay instalación). El listado va ordenado por esta fecha: lo más urgente arriba' },
      td: (m) => <PgUrgenciaCell m={m} urgente={pgMatUrgente(m, hoy)} hoy={hoy} />,
      tdKit: (ls) => <PgUrgenciaCell m={ls[0]} urgente={ls.some((m) => pgMatUrgente(m, hoy))} hoy={hoy} /> },
  ] : null;
  // INC-011: en la ficha de un proyecto, coste por línea (precio de la OC) para quien ve costes.
  const costes = useMatCostes({ proyectoId, dep: rows });
  const rowClass = (m) => `pg-row ${m.estado_material === 'preparado' || m.estado_material === 'enviado' ? 'pg-done' : ''} ${showProyecto && pgMatUrgente(m, hoy) ? 'pg-urgente' : ''}`;
  return (
    <>
      <div className="tbl-wrap"><div className="tbl-scroll" style={{ maxHeight: proyectoId ? 'none' : 'calc(100vh - 340px)' }}>
        <MatTabla className={`tbl tbl-compact pg-tbl ${showProyecto ? 'pg-wide' : ''}`} grupos={[{ key: 'todas', lineas: rows }]}
          guardar={guardar} borrar={borrar} extraCols={extraCols} mostrarCortina rowClass={rowClass} costes={costes} onChanged={reload} onToast={onToast} />
      </div></div>
      <div className="section-title" style={{ marginTop: 18 }}>Añadir material</div>
      <div className="pg-addrow">
        {!proyectoId && <PgProyectoPicker value={f.proyecto_id} proyectos={proyectos} onChange={(v) => setF((x) => ({ ...x, proyecto_id: v || '' }))} style={{ width: 260 }} />}
        {/* INC-001: el material es un artículo del catálogo (obligatorio); lo que no está se crea en
            Catálogo de artículos o va como comentario hasta crearlo. */}
        {comentario
          ? <><MatSinArticulo /><MatComentarioCampo value={f.descripcion} onChange={(v) => setF((x) => ({ ...x, descripcion: v }))} onVolver={volverAlCatalogo} style={{ flex: 2, minWidth: 260 }} /></>
          : <MatArticuloPicker value={f.material_id} onChange={elegirArticulo} onComentario={pasarAComentario} style={{ flex: 2, minWidth: 260 }} placeholder="Artículo: código o nombre…" />}
        {art && art.kit_componentes > 0 && <KitBadge n={art.kit_componentes} corto />}
        {/* GUT-272: medidas en mm, igual que en Expediente (el texto «L × A × H» lo deriva el servidor). */}
        {[['largo', 'Largo'], ['ancho', 'Ancho'], ['alto', 'Alto']].map(([k, l]) => (
          <input key={k} type="number" min="0" step="any" placeholder={`${l} mm`} title={`${l} (mm)`} value={f[k]} onChange={set(k)} style={{ width: 84 }} />
        ))}
        <input placeholder="Cant." value={f.cantidad} onChange={set('cantidad')} style={{ width: 60 }} />
        {!f.de_stock && <PgPicker value={f.proveedor_id} items={provItems} onChange={(v) => setF((x) => ({ ...x, proveedor_id: v || '' }))}
          placeholder="Proveedor…" buscar="Buscar proveedor…" vacio="— Sin proveedor —" allowEmpty style={{ width: 170 }} />}
        <label style={{ display: 'flex', alignItems: 'center', gap: 5, fontSize: 12.5, whiteSpace: 'nowrap' }} title="Ya está en fábrica: no se pide">
          <input type="checkbox" checked={f.de_stock} onChange={(e) => setF((x) => ({ ...x, de_stock: e.target.checked }))} /> De stock
        </label>
        <input placeholder="Observación / comentario" title="Texto libre de la línea (el material es el artículo elegido)" value={f.observacion} onChange={set('observacion')} style={{ flex: 1, minWidth: 160 }} />
        <button className="btn primary sm" onClick={add} disabled={comentario ? !f.descripcion.trim() : !f.material_id} title={f.material_id || comentario ? '' : 'Elige antes el artículo del catálogo (o añádelo como comentario)'}><Icon name="plus" /> {esKit ? 'Añadir kit…' : 'Añadir'}</button>
      </div>
      {verKit && esKit && <KitDesplegarModal material={art} kits={Number(f.cantidad) > 0 ? f.cantidad : 1} onClose={() => setVerKit(false)} onConfirm={crearKit} />}
    </>
  );
};

// GUT-257: tipo de tarea, prioridad del proyecto (listado general), agrupar por tipo y orden por cabecera.
const PgTareasLista = ({ rows, proyectoId, proyectos, instaladores, reload, onToast, showProyecto, oculta, vacio = 'Sin tareas', agrupar, orden, onOrden }) => {
  const tipos = usePgTiposTarea();
  const [f, setF] = React.useState({ proyecto_id: proyectoId || '', tipo_id: '', tarea: '', fecha: '', fecha_fin: '', operario_ids: [] });
  const set = (k) => (e) => setF((x) => ({ ...x, [k]: e.target.value }));
  const add = async () => {
    const pid = proyectoId || f.proyecto_id;
    if (!pid) return onToast('Elige un proyecto', true);
    try { await API.post(`/api/programacion/proyectos/${pid}/tareas`, f); setF((x) => ({ ...x, tarea: '' })); reload(); onToast(oculta && oculta({ ...f, proyecto_id: pid }) ? 'Tarea añadida ✓ (oculta por el filtro)' : 'Tarea añadida ✓'); }
    catch (e) { onToast(e.message, true); }
  };
  const patch = (id, body) => API.patch(`/api/programacion/tareas/${id}`, body).then(reload).catch((e) => onToast(e.message, true));
  const del = (id) => confirm('¿Eliminar esta tarea?') && API.del(`/api/programacion/tareas/${id}`).then(reload).catch((e) => onToast(e.message, true));
  const cls = { pdte: 'warn', ok: 'ok', ko: 'err' };
  return (
    <>
      <div className="tbl-wrap"><div className="tbl-scroll" style={{ maxHeight: proyectoId ? 'none' : 'calc(100vh - 340px)' }}>
        <table className="tbl tbl-compact pg-tbl">
          <thead><tr>{showProyecto && <th style={{ width: 230 }}>Proyecto</th>}<th style={{ width: 150 }}>Tipo</th><th>Tarea</th>
            {onOrden ? <th className="sortable" style={{ width: 130 }} onClick={() => onOrden('fecha')}>Inicio <span className="sort">{orden === 'fecha' ? '↑' : '↕'}</span></th> : <th style={{ width: 130 }}>Inicio</th>}
            <th style={{ width: 130 }}>Fin</th><th style={{ width: 180 }}>Operarios</th><th style={{ width: 100 }}>Estado</th>
            {showProyecto && (onOrden ? <th className="sortable" style={{ width: 96 }} onClick={() => onOrden('prioridad')} title="Ordenar por prioridad del proyecto y luego por fecha">Prioridad <span className="sort">{orden === 'prioridad' ? '↑' : '↕'}</span></th> : <th style={{ width: 96 }}>Prioridad</th>)}
            <th /></tr></thead>
          <tbody>
            {rows.length === 0 && <tr><td colSpan={showProyecto ? 9 : 7} className="muted" style={{ textAlign: 'center' }}>{vacio}</td></tr>}
            {rows.map((t, i) => (
              <React.Fragment key={t.id}>
              {agrupar && (i === 0 || rows[i - 1].tipo_id !== t.tipo_id) && (
                <tr className="pg-tipo-grupo"><td colSpan={showProyecto ? 9 : 7} style={t.tipo_color ? { background: t.tipo_color } : undefined}>
                  {t.tipo_nombre || 'Sin tipo'} <span className="muted">· {rows.filter((x) => x.tipo_id === t.tipo_id).length}</span>
                </td></tr>
              )}
              <tr className="pg-row">
                {showProyecto && <td className="pg-td-text pg-td-proyecto" title={`${t.proyecto_numero} · ${t.proyecto_nombre}`}>
                  <div className="mono strong">{t.proyecto_numero}</div><div><ClienteLink nombre={pgClienteNombre(t.proyecto_numero, t.proyecto_nombre)} proyectoId={t.proyecto_id} /></div>
                </td>}
                <td><PgTipoTareaSelect value={t.tipo_id} tipos={tipos} onSave={(v) => patch(t.id, { tipo_id: v })} /></td>
                <td className="pg-td-text" style={{ minWidth: 240 }}><PgEdit multiline value={t.tarea} onSave={(v) => v && patch(t.id, { tarea: v })} /></td>
                <td><PgEdit type="date" value={t.fecha} onSave={(v) => patch(t.id, { fecha: v })} /></td>
                <td><PgEdit type="date" value={t.fecha_fin} onSave={(v) => patch(t.id, { fecha_fin: v })} title="Vacío = mismo día" /></td>
                <td style={{ minWidth: 150 }}><PgPersonasPicker compact value={t.operario_ids} instaladores={instaladores} onChange={(v) => patch(t.id, { operario_ids: v })} placeholder="Sin operario" /></td>
                <td><PgEdit type="select" className={`pg-pill ${cls[t.estado] || ''}`} value={t.estado} options={[['pdte', 'Pdte'], ['ok', 'OK'], ['ko', 'KO']]} onSave={(v) => v && patch(t.id, { estado: v })} /></td>
                {showProyecto && <td><PgPill v={t.proyecto_prioridad} map={PG_PRIORIDAD_CLS} /></td>}
                <td><button className="icon-btn" onClick={() => del(t.id)}><Icon name="trash" size={12} /></button></td>
              </tr>
              </React.Fragment>
            ))}
          </tbody>
        </table>
      </div></div>
      <div className="section-title" style={{ marginTop: 18 }}>Añadir tarea</div>
      <div className="pg-addrow">
        {!proyectoId && <PgProyectoPicker value={f.proyecto_id} proyectos={proyectos} onChange={(v) => setF((x) => ({ ...x, proyecto_id: v || '' }))} style={{ width: 260 }} />}
        <select value={f.tipo_id} onChange={set('tipo_id')} title="Tipo de tarea" style={{ width: 170 }}>
          <option value="">Tipo…</option>
          {tipos.filter((t) => t.activo).map((t) => <option key={t.id} value={t.id}>{t.nombre}</option>)}
        </select>
        <input placeholder="Tarea (p. ej. cortar tubo 100×100)" value={f.tarea} onChange={set('tarea')} style={{ flex: 2 }} />
        <input type="date" value={f.fecha} onChange={set('fecha')} title="Inicio" />
        <input type="date" value={f.fecha_fin} min={f.fecha} onChange={set('fecha_fin')} title="Fin (vacío = mismo día)" />
        <PgPersonasPicker value={f.operario_ids} instaladores={instaladores} onChange={(v) => setF((x) => ({ ...x, operario_ids: v }))} placeholder="Operarios…" style={{ width: 200 }} />
        <button className="btn primary sm" onClick={add} disabled={!f.tarea}><Icon name="plus" /> Añadir</button>
      </div>
    </>
  );
};

const PgPortesLista = ({ rows, proyectoId, proyectos, instaladores, reload, onToast, showProyecto, compact }) => {
  const [f, setF] = React.useState({ proyecto_id: proyectoId || '', recogida: '', material: '', destino: '', fecha: pgToday(), transporte: '', responsable: '' });
  const set = (k) => (e) => setF((x) => ({ ...x, [k]: e.target.value }));
  const add = async () => {
    try { await API.post('/api/programacion/portes', f); setF((x) => ({ ...x, material: '', recogida: '', destino: '' })); reload(); onToast('Porte añadido ✓'); }
    catch (e) { onToast(e.message, true); }
  };
  const patch = (id, body) => API.patch(`/api/programacion/portes/${id}`, body).then(reload).catch((e) => onToast(e.message, true));
  const del = (id) => confirm('¿Eliminar este porte?') && API.del(`/api/programacion/portes/${id}`).then(reload).catch((e) => onToast(e.message, true));
  return (
    <>
      <div className="tbl-wrap"><div className="tbl-scroll" style={{ maxHeight: compact ? 'none' : 'calc(100vh - 340px)' }}>
        <table className={`tbl tbl-compact pg-tbl ${showProyecto ? 'pg-mid' : ''}`}>
          <thead><tr>
            <th style={{ width: 120 }}>Fecha</th><th>Recogida</th><th>Material</th><th>Destino</th>
            {showProyecto && <th style={{ width: 200 }}>Proyecto</th>}
            <th style={{ width: 130 }}>Transporte</th><th style={{ width: 90 }}>Estado</th><th style={{ width: 160 }}>Responsable</th><th />
          </tr></thead>
          <tbody>
            {rows.length === 0 && <tr><td colSpan={9} className="muted" style={{ textAlign: 'center' }}>Sin portes</td></tr>}
            {rows.map((t) => (
              <tr key={t.id} className={`pg-row ${t.estado === 'ok' ? 'pg-done' : ''}`}>
                <td><PgEdit type="date" value={t.fecha} onSave={(v) => patch(t.id, { fecha: v })} /></td>
                <td className="pg-td-text" style={{ minWidth: 140 }}><PgEdit multiline value={t.recogida} onSave={(v) => patch(t.id, { recogida: v })} /></td>
                <td className="pg-td-text" style={{ minWidth: 200 }}><PgEdit multiline value={t.material} onSave={(v) => patch(t.id, { material: v })} /></td>
                <td className="pg-td-text" style={{ minWidth: 140 }}><PgEdit multiline value={t.destino} onSave={(v) => patch(t.id, { destino: v })} /></td>
                {showProyecto && <td style={{ maxWidth: 220 }}><PgProyectoPicker compact value={t.proyecto_id} proyectos={proyectos} onChange={(v) => patch(t.id, { proyecto_id: v })} /></td>}
                <td><PgEdit value={t.transporte} onSave={(v) => patch(t.id, { transporte: v })} placeholder="coche, furgoneta…" /></td>
                <td><PgEdit type="select" className={`pg-pill ${t.estado === 'ok' ? 'ok' : 'warn'}`} value={t.estado} options={[['pendiente', 'Pendiente'], ['ok', 'OK']]} onSave={(v) => v && patch(t.id, { estado: v })} /></td>
                <td style={{ minWidth: 150 }}><PgResponsablePicker compact value={t.responsable} instaladores={instaladores} onChange={(v) => patch(t.id, { responsable: v })} /></td>
                <td><button className="icon-btn" onClick={() => del(t.id)}><Icon name="trash" size={12} /></button></td>
              </tr>
            ))}
          </tbody>
        </table>
      </div></div>
      <div className="section-title" style={{ marginTop: 18 }}>Añadir porte</div>
      <div className="pg-addrow">
        <input type="date" value={f.fecha} onChange={set('fecha')} />
        <input placeholder="Recogida (origen)" value={f.recogida} onChange={set('recogida')} />
        <input placeholder="Material" value={f.material} onChange={set('material')} style={{ flex: 2 }} />
        <input placeholder="Destino" value={f.destino} onChange={set('destino')} />
        {!proyectoId && <PgProyectoPicker value={f.proyecto_id} proyectos={proyectos} onChange={(v) => setF((x) => ({ ...x, proyecto_id: v || '' }))} placeholder="Proyecto (opcional)…" allowEmpty style={{ width: 240 }} />}
        <input placeholder="Transporte" value={f.transporte} onChange={set('transporte')} style={{ width: 120 }} />
        <PgResponsablePicker value={f.responsable} instaladores={instaladores} onChange={(v) => setF((x) => ({ ...x, responsable: v || '' }))} style={{ width: 170 }} />
        <button className="btn primary sm" onClick={add} disabled={!f.material && !f.destino}><Icon name="plus" /> Añadir</button>
      </div>
    </>
  );
};

// ═══════════════════════════════════════════════════════
// Pestañas globales: Materiales / Portes / Tareas
// ═══════════════════════════════════════════════════════
const PgMaterialesTab = ({ onToast, proyectos, tick, onOpen }) => {
  const [rows, setRows] = React.useState([]);
  // GUT-261: buscador y selector de artículo + resumen "Demanda total por artículo".
  const [buscar, setBuscar] = React.useState('');
  const [texto, setTexto] = React.useState('');
  React.useEffect(() => { const t = setTimeout(() => setTexto(buscar.trim()), 300); return () => clearTimeout(t); }, [buscar]);
  const [articulos, setArticulos] = React.useState([]);
  const [artItems, setArtItems] = React.useState([]);
  const [verTotales, setVerTotales] = React.useState(() => { try { return localStorage.getItem('pg_mat_totales') === '1'; } catch { return false; } });
  React.useEffect(() => { try { localStorage.setItem('pg_mat_totales', verTotales ? '1' : '0'); } catch {} }, [verTotales]);
  const [estado, setEstado] = React.useState('');
  const [pendientes, setPendientes] = React.useState(true);
  // Filtros multiselección (mismo selector que el calendario). Vacío = todos. Se recuerdan.
  const [clientes, setClientes] = React.useState(() => { try { const v = JSON.parse(localStorage.getItem('pg_mat_cli') || '[]'); return Array.isArray(v) ? v.map(String) : []; } catch { return []; } });
  const [provs, setProvs] = React.useState(() => { try { const v = JSON.parse(localStorage.getItem('pg_mat_prov') || '[]'); return Array.isArray(v) ? v.map(String) : []; } catch { return []; } });
  const [proveedores, setProveedores] = React.useState([]);
  React.useEffect(() => { try { localStorage.setItem('pg_mat_cli', JSON.stringify(clientes)); localStorage.setItem('pg_mat_prov', JSON.stringify(provs)); } catch {} }, [clientes, provs]);
  // Mismos parámetros para el listado y los exports. Los multi van repetidos (?proveedor=a&proveedor=b).
  const qs = React.useMemo(() => {
    const u = new URLSearchParams(); if (estado) u.set('estado', estado); if (pendientes) u.set('pendientes', '1');
    clientes.forEach((id) => u.append('proyecto_id', id)); provs.forEach((v) => u.append('proveedor', v));
    if (texto) u.set('q', texto); articulos.forEach((k) => u.append('articulo', k));
    return u.toString();
  }, [estado, pendientes, clientes, provs, texto, articulos]);
  const load = React.useCallback(() => {
    API.get(`/api/programacion/materiales${qs ? '?' + qs : ''}`).then(setRows).catch((e) => onToast(e.message, true));
  }, [qs]);
  React.useEffect(() => { load(); }, [load, tick]);
  React.useEffect(() => { API.get('/api/programacion/materiales/proveedores').then(setProveedores).catch(() => {}); }, [tick]);
  React.useEffect(() => { API.get(`/api/programacion/materiales/articulos${pendientes ? '?pendientes=1' : ''}`).then(setArtItems).catch(() => {}); }, [tick, pendientes]);
  usePgAutoRefresh(load);
  const exportar = (ext) => {
    API.download(`/api/programacion/materiales/export/${ext}${qs ? '?' + qs : ''}`, `materiales_${pgToday()}.${ext === 'excel' ? 'xlsx' : 'pdf'}`)
      .catch((e) => onToast(e.message || 'Error al exportar', true));
  };
  const provItems = React.useMemo(() => proveedores.map((v) => ({ id: v, label: v })), [proveedores]);
  return (
    <>
      <div className="toolbar">
        <div className="seg">
          <button className={estado === '' ? 'on' : ''} onClick={() => setEstado('')}>Todo</button>
          {Object.entries(window.MAT_ESTADOS).filter(([k]) => !pendientes || (k !== 'preparado' && k !== 'enviado')).map(([k, e]) => <button key={k} className={estado === k ? 'on' : ''} onClick={() => setEstado(k)}>{e.label}</button>)}
        </div>
        <PgProyectoPicker multi value={clientes} proyectos={proyectos} onChange={(v) => setClientes((v || []).map(String))} placeholder="Todos los clientes" vacio="— Todos —" allowEmpty style={{ width: 260 }} />
        <PgPicker multi value={provs} items={provItems} onChange={(v) => setProvs((v || []).map(String))} placeholder="Todos los proveedores" buscar="Buscar proveedor…" vacio="— Todos —" allowEmpty style={{ width: 220 }} />
        <input placeholder="Buscar artículo, referencia o expediente…" value={buscar} onChange={(e) => setBuscar(e.target.value)} style={{ width: 200 }} />
        <PgPicker multi value={articulos} items={artItems} onChange={(v) => setArticulos((v || []).map(String))} placeholder="Todos los artículos" buscar="Buscar artículo o referencia…" vacio="— Todos —" allowEmpty style={{ width: 220 }} />
        {(clientes.length > 0 || provs.length > 0 || articulos.length > 0 || buscar) && <button className="btn sm" onClick={() => { setClientes([]); setProvs([]); setArticulos([]); setBuscar(''); }}>Limpiar</button>}
        <div className="tb-spacer" />
        <button className={`btn sm ${verTotales ? 'primary' : ''}`} onClick={() => setVerTotales((v) => !v)} title="Suma de cantidades por artículo de las líneas filtradas"><Icon name="layers" /> Totales por artículo</button>
        <div className="seg">
          <button className={pendientes ? 'on' : ''} onClick={() => setPendientes(true)}>Pendientes</button>
          <button className={!pendientes ? 'on' : ''} onClick={() => setPendientes(false)}>Todos</button>
        </div>
        <button className="btn sm" onClick={() => exportar('excel')} title="Descargar el listado filtrado en Excel"><Icon name="download" /> Excel</button>
        <button className="btn sm" onClick={() => exportar('pdf')} title="Descargar el listado filtrado en PDF"><Icon name="download" /> PDF</button>
      </div>
      {verTotales && <MatTotalesPanel url={`/api/programacion/materiales/totales${qs ? '?' + qs : ''}`} version={rows} onToast={onToast}
        elegidos={articulos} onElegir={(k) => setArticulos((a) => (a.includes(k) ? a.filter((x) => x !== k) : [...a, k]))} />}
      <PgMaterialesLista rows={rows} proyectos={proyectos} reload={load} onToast={onToast} showProyecto onOpen={onOpen} />
    </>
  );
};

const PgPortesTab = ({ onToast, proyectos, instaladores, tick }) => {
  const [rows, setRows] = React.useState([]);
  const [pendientes, setPendientes] = React.useState(true);
  const load = React.useCallback(() => API.get('/api/programacion/portes', pendientes ? { pendientes: '1' } : undefined).then(setRows).catch((e) => onToast(e.message, true)), [pendientes]);
  React.useEffect(() => { load(); }, [load, tick]);
  usePgAutoRefresh(load);
  return (
    <>
      <div className="toolbar">
        <div className="tb-spacer" />
        <div className="seg">
          <button className={pendientes ? 'on' : ''} onClick={() => setPendientes(true)}>Pendientes</button>
          <button className={!pendientes ? 'on' : ''} onClick={() => setPendientes(false)}>Todos</button>
        </div>
      </div>
      <PgPortesLista rows={rows} proyectos={proyectos} instaladores={instaladores} reload={load} onToast={onToast} showProyecto />
    </>
  );
};

// GUT-246: dos vistas. "Fábrica" = tareas de fábrica (con descripción). "Mediciones y visitas" = las
// fechas de todos los proyectos (medición, repaso, instalación extra, otro), que se crean en ficha › Fechas.
const PgTareasTab = ({ onToast, proyectos, instaladores, tick, onOpen }) => {
  const [vista, setVista] = React.useState(() => { try { return localStorage.getItem('pg_tareas_vista') || 'fabrica'; } catch { return 'fabrica'; } });
  const cambiar = (v) => { setVista(v); try { localStorage.setItem('pg_tareas_vista', v); } catch {} };
  const [rows, setRows] = React.useState([]);
  const [pendientes, setPendientes] = React.useState(true);
  // GUT-250: filtro por persona (recordado) y buscador por nº, cliente o texto de la tarea.
  const [ops, setOps] = usePgFiltroPersonas('pg_tareas_inst');
  // GUT-257: filtro por tipo (recordado) y orden por fecha o por prioridad del proyecto.
  const tipos = usePgTiposTarea();
  const [tiposSel, setTiposSel] = usePgFiltroPersonas('pg_tareas_tipo');
  const [orden, setOrdenState] = React.useState(() => { try { return localStorage.getItem('pg_tareas_orden') || 'fecha'; } catch { return 'fecha'; } });
  const setOrden = (v) => { setOrdenState(v); try { localStorage.setItem('pg_tareas_orden', v); } catch {} };
  const [search, setSearch] = React.useState('');
  const [q, setQ] = React.useState('');
  React.useEffect(() => { const t = setTimeout(() => setQ(search.trim()), 250); return () => clearTimeout(t); }, [search]);
  const params = React.useMemo(() => {
    const o = {};
    if (pendientes) o.pendientes = '1';
    if (ops.length) o.operario_id = ops.join(',');
    if (tiposSel.length) o.tipo_id = tiposSel.join(',');
    if (q) o.search = q;
    return o;
  }, [pendientes, ops, tiposSel, q]);
  const agrupar = tiposSel.length > 0;
  const vistas = React.useMemo(() => pgOrdenTareas(rows, orden, agrupar ? tipos : null), [rows, orden, agrupar, tipos]);
  const tipoOpts = [{ value: 'sin', label: 'Sin tipo' }, ...tipos.filter((t) => t.activo || tiposSel.includes(String(t.id))).map((t) => ({ value: String(t.id), label: t.nombre }))];
  const filtrosTxt = [
    tiposSel.length && `Tipo: ${tiposSel.map((v) => (tipoOpts.find((o) => o.value === v) || {}).label).filter(Boolean).join(', ')}`,
    ops.length && `Persona: ${ops.map((v) => (v === 'sin' ? 'Sin operario' : (instaladores.find((i) => String(i.id) === v) || {}).nombre)).filter(Boolean).join(', ')}`,
    q && `Búsqueda: "${q}"`, pendientes ? 'Solo pendientes' : 'Todas',
  ].filter(Boolean).join(' · ');
  const load = React.useCallback(() => API.get('/api/programacion/tareas', params).then(setRows).catch((e) => onToast(e.message, true)), [params]);
  // ¿La tarea recién añadida queda fuera de los filtros puestos?
  const oculta = (t) => {
    const ids = (t.operario_ids || []).map(String);
    if (ops.length && !(ids.length ? ids.some((id) => ops.includes(id)) : ops.includes('sin'))) return true;
    if (tiposSel.length && !tiposSel.includes(t.tipo_id ? String(t.tipo_id) : 'sin')) return true;
    if (!q) return false;
    const p = proyectos.find((x) => String(x.id) === String(t.proyecto_id));
    return ![t.tarea, p?.numero, p?.nombre].some((x) => String(x || '').toLowerCase().includes(q.toLowerCase()));
  };
  const loadFabrica = React.useCallback(() => { if (vista === 'fabrica') load(); }, [vista, load]);
  React.useEffect(() => { loadFabrica(); }, [loadFabrica, tick]);
  usePgAutoRefresh(loadFabrica);
  const seg = (
    <div className="seg">
      <button className={vista === 'fabrica' ? 'on' : ''} onClick={() => cambiar('fabrica')}>Tareas de proyecto</button>
      <button className={vista === 'fechas' ? 'on' : ''} onClick={() => cambiar('fechas')}>Mediciones y visitas</button>
    </div>
  );
  if (vista === 'fechas') return <PgFechasTab onToast={onToast} instaladores={instaladores} proyectos={proyectos} tick={tick} onOpen={onOpen} seg={seg} />;
  return (
    <>
      <div className="toolbar">
        {seg}
        <div className="tb-divider" />
        <div className="tb-search" style={{ minWidth: 320 }}><Icon name="search" /><input placeholder="Nº, cliente, tarea…" value={search} onChange={(e) => setSearch(e.target.value)} /></div>
        <MultiChip icon="wrench" label="Persona" options={pgPersonaOpts(instaladores, 'Sin operario')} selected={ops} onChange={setOps} />
        <MultiChip label="Tipo" options={tipoOpts} selected={tiposSel} onChange={setTiposSel} />
        <div className="seg" title="Orden del listado">
          <button className={orden === 'fecha' ? 'on' : ''} onClick={() => setOrden('fecha')}>Por fecha</button>
          <button className={orden === 'prioridad' ? 'on' : ''} onClick={() => setOrden('prioridad')}>Por prioridad</button>
        </div>
        <div className="tb-spacer" />
        <span className="muted" style={{ fontSize: 11 }}>{rows.length} tareas</span>
        <div className="seg">
          <button className={pendientes ? 'on' : ''} onClick={() => setPendientes(true)}>Pendientes</button>
          <button className={!pendientes ? 'on' : ''} onClick={() => setPendientes(false)}>Todas</button>
        </div>
        <button className="btn sm" onClick={() => pgExcelTareas(vistas, onToast)} disabled={!rows.length} title="Descargar en Excel las tareas que se ven (con los filtros puestos)"><Icon name="download" /> Excel</button>
      </div>
      <div className="pg-print-filtros">{filtrosTxt} · {rows.length} tareas</div>
      <PgTareasLista rows={vistas} proyectos={proyectos} instaladores={instaladores} reload={load} onToast={onToast} showProyecto
        agrupar={agrupar} orden={orden} onOrden={setOrden}
        oculta={oculta} vacio={ops.length || tiposSel.length || q ? 'Sin tareas con estos filtros' : 'Sin tareas'} />
    </>
  );
};

// Listado global de fechas (medición, repaso, instalación extra, otro) de todos los proyectos, con filtros.
// Se editan en línea igual que en ficha › Fechas. GUT-258: también se crean aquí (misma fecha que ficha › Fechas).
const PgFechasTab = ({ onToast, instaladores, proyectos, tick, onOpen, seg }) => {
  const [rows, setRows] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [pendientes, setPendientes] = React.useState(true);
  const [tipo, setTipo] = React.useState(['medicion']);
  const [inst, setInst] = usePgFiltroPersonas('pg_fechas_inst');
  const [search, setSearch] = React.useState('');
  const [q, setQ] = React.useState('');
  React.useEffect(() => { const t = setTimeout(() => setQ(search), 250); return () => clearTimeout(t); }, [search]);
  const params = React.useMemo(() => {
    const o = {};
    if (pendientes) o.pendientes = '1';
    if (tipo.length) o.tipo = tipo.join(',');
    if (inst.length) o.instalador_id = inst.join(',');
    if (q) o.search = q;
    return o;
  }, [pendientes, tipo, inst, q]);
  const load = React.useCallback(() => API.get('/api/programacion/eventos', params).then((r) => { setRows(r); setLoading(false); }).catch((e) => onToast(e.message, true)), [params]);
  React.useEffect(() => { load(); }, [load, tick]);
  usePgAutoRefresh(load);
  const patch = (id, body) => API.patch(`/api/programacion/eventos/${id}`, body).then(load).catch((e) => onToast(e.message, true));
  const del = (id) => confirm('¿Quitar esta fecha?') && API.del(`/api/programacion/eventos/${id}`).then(load).catch((e) => onToast(e.message, true));
  const tipoOpts = Object.entries(PG_TIPOS_EVENTO).map(([value, label]) => ({ value, label: value === 'instalacion' ? 'Instalación (extra)' : label }));
  const instOpts = pgPersonaOpts(instaladores);
  const hoy = pgToday();
  // GUT-258: alta aquí mismo. Crea lo mismo que ficha › Fechas (POST al proyecto), así sale en la ficha y en el calendario.
  const [nf, setNf] = React.useState(() => ({ proyecto_id: null, tipo: tipo.length === 1 ? tipo[0] : 'medicion', fecha_inicio: hoy, fecha_fin: '', instalador_ids: [], nota: '' }));
  const setN = (k) => (e) => setNf((x) => ({ ...x, [k]: e.target.value }));
  const add = async () => {
    try {
      const { proyecto_id, ...body } = nf;
      await API.post(`/api/programacion/proyectos/${proyecto_id}/eventos`, { ...body, fecha_fin: body.fecha_fin || body.fecha_inicio });
      const ids = nf.instalador_ids.map(String);
      const p = proyectos.find((x) => String(x.id) === String(proyecto_id));
      const fuera = (tipo.length && !tipo.includes(nf.tipo)) || (inst.length && !(ids.length ? ids.some((i) => inst.includes(i)) : inst.includes('sin')))
        || (q && ![p?.numero, p?.nombre, p?.ubicacion].some((x) => String(x || '').toLowerCase().includes(q.toLowerCase())));
      setNf((x) => ({ ...x, proyecto_id: null, nota: '', fecha_fin: '' }));
      load(); onToast(fuera ? 'Fecha añadida ✓ (no sale en la lista con los filtros o la búsqueda puestos)' : 'Fecha añadida ✓');
    } catch (e) { onToast(e.message, true); }
  };
  return (
    <>
      <div className="toolbar">
        {seg}
        <div className="tb-divider" />
        <div className="tb-search" style={{ minWidth: 320 }}><Icon name="search" /><input placeholder="Nº, cliente, ubicación…" value={search} onChange={(e) => setSearch(e.target.value)} /></div>
        <MultiChip label="Tipo" options={tipoOpts} selected={tipo} onChange={setTipo} />
        <MultiChip icon="wrench" label="Persona" options={instOpts} selected={inst} onChange={setInst} />
        <div className="tb-spacer" />
        <span className="muted" style={{ fontSize: 11 }}>{rows.length} fechas</span>
        <div className="seg">
          <button className={pendientes ? 'on' : ''} onClick={() => setPendientes(true)}>Pendientes</button>
          <button className={!pendientes ? 'on' : ''} onClick={() => setPendientes(false)}>Todas</button>
        </div>
        <button className="btn sm" onClick={() => pgExcelFechas(rows, onToast)} disabled={!rows.length} title="Descargar en Excel las fechas que se ven (con los filtros puestos)"><Icon name="download" /> Excel</button>
      </div>
      <div className="pg-addrow" style={{ marginBottom: 10 }}>
        <strong style={{ fontSize: 12.5 }}>Añadir medición/visita:</strong>
        <PgProyectoPicker proyectos={proyectos} value={nf.proyecto_id} onChange={(v) => setNf((x) => ({ ...x, proyecto_id: v }))} placeholder="Proyecto (nº o cliente)…" style={{ width: 280 }} />
        <select value={nf.tipo} onChange={setN('tipo')}>{Object.entries(PG_TIPOS_EVENTO).map(([k, l]) => <option key={k} value={k}>{k === 'instalacion' ? 'Instalación (extra)' : l}</option>)}</select>
        <input type="date" value={nf.fecha_inicio} onChange={setN('fecha_inicio')} title="Inicio" />
        <input type="date" value={nf.fecha_fin} onChange={setN('fecha_fin')} title="Fin (vacío = mismo día)" />
        <PgPersonasPicker value={nf.instalador_ids} instaladores={instaladores} onChange={(v) => setNf((x) => ({ ...x, instalador_ids: v }))} placeholder="Persona(s)…" style={{ width: 200 }} />
        <input placeholder="Nota" value={nf.nota} onChange={setN('nota')} style={{ flex: 1, minWidth: 160 }} />
        <button className="btn primary sm" onClick={add} disabled={!nf.proyecto_id || !nf.fecha_inicio} title={nf.proyecto_id ? '' : 'Elige primero el proyecto'}><Icon name="plus" /> Añadir</button>
      </div>
      <div className="tbl-wrap"><div className="tbl-scroll" style={{ maxHeight: 'calc(100vh - 340px)' }}>
        <table className="tbl pg-tbl">
          <thead><tr>
            <th style={{ width: 110 }}>Inicio</th><th style={{ width: 110 }}>Fin</th><th style={{ width: 120 }}>Tipo</th>
            <th style={{ width: 84 }}>Nº</th><th style={{ minWidth: 220 }}>Proyecto / cliente</th><th style={{ minWidth: 160 }}>Ubicación</th>
            <th style={{ minWidth: 170 }}>Personas</th><th style={{ width: 50 }}>Op.</th><th style={{ minWidth: 220 }}>Nota</th><th style={{ width: 120 }}>Estado</th><th style={{ width: 36 }} />
          </tr></thead>
          <tbody>
            {loading && <tr><td colSpan={11} className="muted" style={{ textAlign: 'center', padding: 30 }}>Cargando…</td></tr>}
            {!loading && rows.length === 0 && <tr><td colSpan={11} className="muted" style={{ textAlign: 'center', padding: 30 }}>Sin fechas con estos filtros. Añade una en la fila de arriba o desde la ficha del proyecto › Fechas.</td></tr>}
            {!loading && rows.map((e) => (
              <tr key={e.id} className={`pg-row ${e.estado !== 'programado' ? 'pg-done' : ''}`} onDoubleClick={() => onOpen(e.proyecto_id)}>
                <td className={e.estado === 'programado' && e.fecha_inicio < hoy ? 'pg-fc err' : ''} title={e.estado === 'programado' && e.fecha_inicio < hoy ? 'Fecha pasada y sigue pendiente' : ''}><PgEdit type="date" value={e.fecha_inicio} onSave={(v) => v && patch(e.id, { fecha_inicio: v })} /></td>
                <td><PgEdit type="date" value={e.fecha_fin} onSave={(v) => v && patch(e.id, { fecha_fin: v })} /></td>
                <td><PgEdit type="select" value={e.tipo} options={Object.entries(PG_TIPOS_EVENTO)} onSave={(v) => v && patch(e.id, { tipo: v })} /></td>
                <td className="num strong" style={{ cursor: 'pointer', textDecoration: 'underline' }} onClick={() => onOpen(e.proyecto_id)} title="Abrir la ficha del proyecto">{e.proyecto_numero}</td>
                <td className="pg-td-text">
                  <div className="strong" style={{ cursor: 'pointer' }} onClick={() => onOpen(e.proyecto_id)}>{e.proyecto_nombre}</div>
                  {e.cliente_telefono && <div style={{ fontSize: 11.5 }}><a href={`tel:${e.cliente_telefono.replace(/\s+/g, '')}`} onClick={(ev) => ev.stopPropagation()}>☎ {e.cliente_telefono}</a>{e.cliente_nombre ? <span className="muted"> · <ClienteLink nombre={e.cliente_nombre} proyectoId={e.proyecto_id} /></span> : null}</div>}
                </td>
                <td className="pg-td-text muted">
                  <div>{[e.ubicacion, e.provincia].filter(Boolean).join(', ') || '—'}</div>
                  {e.cliente_direccion && <div style={{ fontSize: 11 }}>{e.cliente_direccion}</div>}
                </td>
                <td><PgPersonasPicker compact value={e.instalador_ids} instaladores={instaladores} onChange={(v) => patch(e.id, { instalador_ids: v })} /></td>
                <td><PgEdit type="num" value={e.num_operarios} onSave={(v) => patch(e.id, { num_operarios: v })} /></td>
                <td className="pg-td-text"><PgEdit multiline value={e.nota} onSave={(v) => patch(e.id, { nota: v })} placeholder="Nota" /></td>
                <td><PgEdit type="select" value={e.estado} options={[['programado', 'Programado'], ['hecho', 'Hecho'], ['ko', 'KO']]} onSave={(v) => v && patch(e.id, { estado: v })} /></td>
                <td><button className="icon-btn" onClick={() => del(e.id)} title="Quitar"><Icon name="trash" size={12} /></button></td>
              </tr>
            ))}
          </tbody>
        </table>
      </div></div>
      <div className="muted" style={{ fontSize: 11, marginTop: 8 }}>Lo que añades aquí es lo mismo que ficha del proyecto › Fechas: sale en la ficha y en el Calendario, en la fila de cada persona (sin persona, en "Sin asignar"). Clic en el nº o doble clic en la fila para abrir la ficha.</div>
    </>
  );
};

// ═══════════════════════════════════════════════════════
// Calendario: semana por instalador / gantt mensual
// ═══════════════════════════════════════════════════════
const PgCalendarioTab = ({ onToast, instaladores, proyectos, onOpen, tick }) => {
  const [modo, setModo] = React.useState('semana');
  const [lunes, setLunes] = React.useState(pgMonday(pgToday()));
  const [mes, setMes] = React.useState(pgToday().slice(0, 7));
  const [data, setData] = React.useState({ items: [], festivos: [], dias_laborables: [1, 2, 3, 4, 5], ausencias: [] });
  // Filtro por instaladores, multiselección (se recuerda). Vacío = todos. Los portes no tienen instalador y se ocultan al filtrar.
  const [inst, setInst] = React.useState(() => { try { const v = JSON.parse(localStorage.getItem('pg_cal_inst') || '[]'); return Array.isArray(v) ? v.map(String) : []; } catch { return []; } });
  React.useEffect(() => { try { localStorage.setItem('pg_cal_inst', JSON.stringify(inst)); } catch {} }, [inst]);
  const instSet = React.useMemo(() => new Set(inst), [inst]);
  // GUT-258: filtro por proyectos, multiselección (se recuerda). Vacío = todos. Se combina con el de instaladores.
  const [proj, setProj] = usePgFiltroPersonas('pg_cal_proj');
  const projSet = React.useMemo(() => new Set(proj), [proj]);
  const items = React.useMemo(() => data.items
    .filter((it) => !inst.length || (it.instalador_ids || []).some((id) => instSet.has(String(id))))
    .filter((it) => !proj.length || (it.proyecto_id && projSet.has(String(it.proyecto_id)))), [data.items, instSet, projSet]);
  const projNombre = (id) => { const p = (proyectos || []).find((x) => String(x.id) === id); return p ? `${p.numero} · ${p.nombre.replace(/^\d+[.,]?\s*/, '')}` : `#${id}`; };
  const instFiltrados = React.useMemo(() => (inst.length ? instaladores.filter((i) => instSet.has(String(i.id))) : instaladores), [instaladores, instSet]);
  const rango = React.useMemo(() => {
    if (modo === 'semana') return { desde: lunes, hasta: pgAddDays(lunes, 6) };
    const [y, m] = mes.split('-').map(Number);
    const last = new Date(Date.UTC(y, m, 0)).getUTCDate();
    return { desde: `${mes}-01`, hasta: `${mes}-${String(last).padStart(2, '0')}` };
  }, [modo, lunes, mes]);
  const load = React.useCallback(() => API.get('/api/programacion/calendario', rango).then(setData).catch((e) => onToast(e.message, true)), [rango]);
  React.useEffect(() => { load(); }, [load, tick]);
  usePgAutoRefresh(load);
  const cal = { festivos: new Set(data.festivos), dias: new Set(data.dias_laborables) };
  const hoy = pgToday();

  const nav = (n) => (modo === 'semana' ? setLunes(pgAddDays(lunes, 7 * n)) : setMes(() => { const [y, m] = mes.split('-').map(Number); const d = new Date(Date.UTC(y, m - 1 + n, 1)); return d.toISOString().slice(0, 7); }));
  const titulo = modo === 'semana' ? `Semana del ${pgFmtLong(lunes)} al ${pgFmtLong(pgAddDays(lunes, 4))}` : pgMesLabel(mes);

  return (
    <>
      <div className="toolbar">
        <div className="seg">
          <button className={modo === 'semana' ? 'on' : ''} onClick={() => setModo('semana')}>Semana</button>
          <button className={modo === 'mes' ? 'on' : ''} onClick={() => setModo('mes')}>Mes</button>
        </div>
        <div className="tb-divider" />
        <button className="icon-btn" onClick={() => nav(-1)}><Icon name="chevron_left" /></button>
        <strong className="pg-cal-titulo" style={{ fontSize: 13, minWidth: 220, textAlign: 'center' }}>{titulo}</strong>
        <button className="icon-btn" onClick={() => nav(1)}><Icon name="chevron_right" /></button>
        <button className="btn ghost sm" onClick={() => { setLunes(pgMonday(hoy)); setMes(hoy.slice(0, 7)); }}>Hoy</button>
        <div className="tb-divider" />
        <PgPersonasPicker value={inst} instaladores={instaladores} onChange={(v) => setInst((v || []).map(String))} placeholder="Todos los instaladores" vacio="— Todos —" style={{ width: 260 }} />
        {inst.length > 0 && <button className="btn ghost sm" onClick={() => setInst([])} title="Quitar filtro"><Icon name="x" size={12} /></button>}
        <PgProyectoPicker multi allowEmpty vacio="— Todos los proyectos —" proyectos={proyectos} value={proj} onChange={(v) => setProj((v || []).map(String))}
          placeholder="Todos los proyectos" style={{ width: 240 }} />
        <div className="tb-spacer" />
        <span className="muted" style={{ fontSize: 11 }}>{items.length} trabajos{(inst.length > 0 || proj.length > 0) && items.length !== data.items.length ? ` de ${data.items.length}` : ''} · clic para abrir la ficha</span>
      </div>
      {proj.length > 0 && (
        <div className="row-flex pg-no-print" style={{ gap: 6, flexWrap: 'wrap', marginBottom: 8 }}>
          <span className="muted" style={{ fontSize: 11 }}>Proyectos:</span>
          {proj.map((id) => (
            <span key={id} className="chip active">{projNombre(id)}<span className="x" title="Quitar" onClick={() => setProj(proj.filter((x) => x !== id))}><Icon name="x" size={11} /></span></span>
          ))}
          <button className="btn ghost sm" onClick={() => setProj([])}>Limpiar</button>
        </div>
      )}
      {modo === 'semana' ? <PgSemana lunes={lunes} items={items} cal={cal} ausencias={data.ausencias} instaladores={instFiltrados} onOpen={onOpen} hoy={hoy} />
        : <PgMes mes={mes} items={items} cal={cal} onOpen={onOpen} hoy={hoy} />}
      <div className="pg-legend">
        {Object.entries(PG_TIPOS_CAL).map(([k, l]) => <span key={k}><i className={`pg-dot t-${k}`} />{l}{k === 'instalacion' ? ' (color del instalador)' : ''}</span>)}
        <span><i className="pg-dot" style={{ background: 'var(--warn)' }} />Material o fabricación pendiente</span>
      </div>
    </>
  );
};

const PgChip = ({ it, color, onOpen }) => {
  const bloqueado = it.origen === 'proyecto' && (!it.material_completo || !it.fabricacion_terminada);
  const bg = it.tipo === 'instalacion' ? (color || 'var(--carbon)') : undefined;
  const extra = it.tipo === 'prevista' ? `\n${it.nota ? `${it.nota}\n` : ''}${it.planificada ? `Instalación planificada: ${pgFmtLong(it.planificada)}` : 'Sin instalación planificada'}` : '';
  const hecho = (it.origen === 'porte' || it.origen === 'tarea') && it.evento_estado === 'hecho';
  return (
    <div className={`pg-chip t-${it.tipo} ${it.evento_estado === 'ko' ? 'ko' : ''} ${hecho ? 'done' : ''}`} style={bg ? { background: bg } : undefined}
      title={`${it.numero ? `${it.numero} · ` : ''}${it.nombre}\n${PG_TIPOS_CAL[it.tipo]} ${pgFmtLong(it.fecha_inicio)} → ${pgFmtLong(it.fecha_fin)}${it.instalador_nombre ? `\n${it.instalador_nombre}` : ''}${it.num_operarios ? ` · ${it.num_operarios} op.` : ''}${it.transporte ? ` · ${it.transporte}` : ''}${it.nota ? `\n${it.nota}` : ''}\n${hecho ? 'Hecho' : it.estado || ''}${extra}`}
      onClick={() => it.proyecto_id && onOpen(it.proyecto_id)}>
      {bloqueado && <i className="pg-warn" title="Material o fabricación pendiente" />}
      {it.numero && <span className="mono">{it.numero}</span>} {(it.nombre || '').replace(/^\d+[.,]?\s*/, '')}{it.tipo !== 'instalacion' ? <em> · {PG_TIPOS_CAL[it.tipo]}</em> : null}
      {it.ubicacion ? <small> {it.ubicacion}</small> : null}
    </div>
  );
};

const PgSemana = ({ lunes, items, cal, ausencias, instaladores, onOpen, hoy }) => {
  const [todos, setTodos] = React.useState(false);
  const dias = [0, 1, 2, 3, 4, 5, 6].map((n) => pgAddDays(lunes, n));
  // Se muestran los días laborables configurados; los demás solo si tienen algo programado.
  const cols = dias.filter((d, n) => n < 5 || !pgOff(d, cal) || items.some((it) => pgPinta(it, d)));
  const ausenciaDe = (instId, d) => ausencias.find((a) => a.instalador_id === instId && d >= a.desde && d <= a.hasta);
  const filas = [];
  const byInst = new Map();
  // Cada trabajo sale en la fila de cada persona asignada. Sin nadie: "Sin asignar" (o "Fábrica" si es tarea).
  const push = (k, it) => { if (!byInst.has(k)) byInst.set(k, []); byInst.get(k).push(it); };
  for (const it of items) {
    if (it.origen === 'porte') push('portes', it);
    else if (it.origen === 'prevista') push('cliente', it);
    else if (it.instalador_ids?.length) it.instalador_ids.forEach((id) => push(id, it));
    else push(it.origen === 'tarea' ? 'fabrica' : 0, it);
  }
  for (const i of instaladores) if ((todos && i.activo) || byInst.has(i.id)) filas.push({ id: i.id, nombre: i.nombre, color: i.color, items: byInst.get(i.id) || [] });
  if (byInst.has(0)) filas.push({ id: 0, nombre: 'Sin asignar', items: byInst.get(0) });
  if (byInst.has('fabrica')) filas.push({ id: 'fabrica', nombre: 'Fábrica (sin operario)', color: '#6d28d9', items: byInst.get('fabrica') });
  if (byInst.has('portes')) filas.push({ id: 'portes', nombre: 'Portes', color: '#15803d', items: byInst.get('portes') });
  if (byInst.has('cliente')) filas.push({ id: 'cliente', nombre: 'Fechas pedidas por el cliente', color: 'var(--ink-3)', items: byInst.get('cliente') });
  return (
    <>
    <div className="row-flex pg-no-print" style={{ justifyContent: 'flex-end', marginBottom: 6 }}>
      <label className="pg-check-lbl" style={{ fontSize: 12, paddingTop: 0 }}><input type="checkbox" className="pg-ck" checked={todos} onChange={(e) => setTodos(e.target.checked)} /> Ver todos los instaladores</label>
    </div>
    <div className="pg-cal-wrap"><div className="pg-cal" style={{ gridTemplateColumns: `170px repeat(${cols.length}, minmax(150px, 1fr))` }}>
      <div className="hd">Instalador</div>
      {cols.map((d) => <div key={d} className={`hd ${d === hoy ? 'today' : ''} ${pgOff(d, cal) ? 'off' : ''}`}>{PG_DIAS[pgDow(d)]} {d.slice(8, 10)}{cal.festivos.has(d) ? ' · festivo' : ''}</div>)}
      {filas.length === 0 && <div className="muted" style={{ gridColumn: `1 / span ${cols.length + 1}`, padding: 20, textAlign: 'center' }}>Nada programado esta semana</div>}
      {filas.map((f) => (
        <React.Fragment key={f.id}>
          <div className="inst"><i className="pg-dot" style={{ background: f.color || 'var(--ink-4)' }} />{f.nombre}</div>
          {cols.map((d) => { const au = typeof f.id === 'number' && f.id ? ausenciaDe(f.id, d) : null; return (
            <div key={d} className={`cell ${d === hoy ? 'today' : ''} ${pgOff(d, cal) || au ? 'off' : ''}`}>
              {au && <div className="pg-ausencia" title={au.motivo || 'Ausencia'}>{au.motivo || 'Ausencia'}</div>}
              {f.items.filter((it) => pgPinta(it, d)).map((it) => <PgChip key={`${it.origen}${it.evento_id || it.porte_id || it.tarea_id || it.proyecto_id}`} it={it} color={f.color} onOpen={onOpen} />)}
            </div>
          ); })}
        </React.Fragment>
      ))}
    </div></div>
    </>
  );
};

const PgMes = ({ mes, items, cal, onOpen, hoy }) => {
  const [y, m] = mes.split('-').map(Number);
  const last = new Date(Date.UTC(y, m, 0)).getUTCDate();
  const dias = Array.from({ length: last }, (_, i) => `${mes}-${String(i + 1).padStart(2, '0')}`);
  const sorted = [...items].sort((a, b) => (a.instalador_nombre || 'zz').localeCompare(b.instalador_nombre || 'zz') || a.fecha_inicio.localeCompare(b.fecha_inicio));
  return (
    <div className="tbl-wrap"><div className="tbl-scroll" style={{ maxHeight: 'calc(100vh - 300px)' }}>
      <table className="tbl pg-gantt">
        <thead><tr>
          <th style={{ minWidth: 220 }}>Proyecto</th><th style={{ minWidth: 130 }}>Instaladores</th>
          {dias.map((d) => <th key={d} className={`day ${d === hoy ? 'today' : ''} ${pgOff(d, cal) ? 'off' : ''}`} title={cal.festivos.has(d) ? 'Festivo' : ''}>{d.slice(8, 10)}<br /><small>{PG_DIAS[pgDow(d)][0]}</small></th>)}
        </tr></thead>
        <tbody>
          {sorted.length === 0 && <tr><td colSpan={2 + dias.length} className="muted" style={{ textAlign: 'center', padding: 20 }}>Nada programado este mes</td></tr>}
          {sorted.map((it) => (
            <tr key={`${it.origen}${it.evento_id || it.porte_id || it.tarea_id || it.proyecto_id}`} onClick={() => it.proyecto_id && onOpen(it.proyecto_id)} className={(it.origen === 'porte' || it.origen === 'tarea') && it.evento_estado === 'hecho' ? 'pg-done' : ''}>
              <td className="strong truncate" style={{ maxWidth: 260 }} title={it.nota || it.nombre}>
                {it.numero && <span className="mono muted" style={{ marginRight: 6 }}>{it.numero}</span>}{(it.nombre || '').replace(/^\d+[.,]?\s*/, '')}
                {it.tipo !== 'instalacion' && <em className="muted" style={{ fontWeight: 400 }}> · {PG_TIPOS_CAL[it.tipo]}</em>}
              </td>
              <td className="muted">{it.instalador_nombre || '—'}</td>
              {dias.map((d) => {
                const on = pgPinta(it, d);
                const off = pgOff(d, cal);
                return <td key={d} className={`day ${off ? 'off' : ''} ${d === hoy ? 'today' : ''}`}>{on && <i className={`bar t-${it.tipo}`} style={it.tipo === 'instalacion' && it.instaladores?.[0]?.color ? { background: it.instaladores[0].color } : undefined} />}</td>;
              })}
            </tr>
          ))}
        </tbody>
      </table>
    </div></div>
  );
};

// ═══════════════════════════════════════════════════════
// Carga por instalador
// ═══════════════════════════════════════════════════════
const PgCargaTab = ({ onToast, tick }) => {
  const [mes, setMes] = React.useState(pgToday().slice(0, 7));
  const [data, setData] = React.useState(null);
  const load = React.useCallback(() => API.get('/api/programacion/carga', { mes }).then(setData).catch((e) => onToast(e.message, true)), [mes]);
  React.useEffect(() => { load(); }, [load, tick]);
  usePgAutoRefresh(load);
  const nav = (n) => setMes(() => { const [y, m] = mes.split('-').map(Number); return new Date(Date.UTC(y, m - 1 + n, 1)).toISOString().slice(0, 7); });
  const situ = (o, disp) => (!disp ? ['Ausente', 'muted'] : o > 100 ? ['Saturado', 'err'] : o >= 80 ? ['Alta carga', 'warn'] : o >= 50 ? ['Media', 'info'] : ['Disponible', 'ok']);
  return (
    <>
      <div className="toolbar">
        <button className="icon-btn" onClick={() => nav(-1)}><Icon name="chevron_left" /></button>
        <strong style={{ fontSize: 13, minWidth: 180, textAlign: 'center' }}>{pgMesLabel(mes)}</strong>
        <button className="icon-btn" onClick={() => nav(1)}><Icon name="chevron_right" /></button>
        <div className="tb-spacer" />
        {data && <span className="muted" style={{ fontSize: 11 }}>{data.dias_disponibles} días laborables en el mes · Asignados cuenta todos los días del trabajo, también fines de semana y festivos, por eso puede superar el 100 %</span>}
      </div>
      <div className="tbl-wrap"><div className="tbl-scroll" style={{ maxHeight: 'none' }}><table className="tbl">
        <thead><tr><th>Instalador</th><th style={{ width: 120 }}>Disponibles</th><th style={{ width: 170 }}>Asignados</th><th style={{ width: 100 }}>Proyectos</th><th style={{ width: 260 }}>Ocupación</th><th style={{ width: 120 }}>Situación</th></tr></thead>
        <tbody>
          {!data && <tr><td colSpan={6} className="muted" style={{ textAlign: 'center', padding: 20 }}>Cargando…</td></tr>}
          {data && data.instaladores.map((i) => { const [l, c] = situ(i.ocupacion, i.dias_disponibles); return (
            <tr key={i.id} style={{ cursor: 'default' }}>
              <td className="strong"><i className="pg-dot" style={{ background: i.color || 'var(--ink-4)' }} />{i.nombre}</td>
              <td className="num">{i.dias_disponibles}{i.dias_ausencia > 0 && <small className="muted" title="Días de ausencia (vacaciones, baja…) en el mes"> · −{i.dias_ausencia} aus.</small>}</td>
              <td className="num strong">{i.dias_asignados}{i.solapes > 0 && <span className="pill warn" style={{ marginLeft: 6 }} title="Días con más de un trabajo asignado al mismo instalador"><span className="d" />{i.solapes} solape{i.solapes > 1 ? 's' : ''}</span>}</td>
              <td className="num">{i.proyectos}</td>
              <td><div className="row-flex"><span className={`pbar ${c === 'err' ? 'warn' : c === 'ok' ? 'ok' : ''}`} style={{ width: 160 }}><i style={{ width: `${Math.min(100, i.ocupacion)}%` }} /></span><span className="num muted" style={{ fontSize: 11 }}>{i.dias_disponibles ? `${i.ocupacion}%` : '—'}</span></div></td>
              <td><span className={`pill ${c}`}><span className="d" />{l}</span></td>
            </tr>
          ); })}
        </tbody>
      </table></div></div>
    </>
  );
};

// ═══════════════════════════════════════════════════════
// Configuración: instaladores y festivos
// ═══════════════════════════════════════════════════════
const PgConfigTab = ({ onToast, refresh, tick }) => {
  const [inst, setInst] = React.useState([]);
  const [fest, setFest] = React.useState([]);
  const [nuevoInst, setNuevoInst] = React.useState('');
  const [nuevoFest, setNuevoFest] = React.useState({ fecha: '', nombre: '' });
  const [usuarios, setUsuarios] = React.useState([]); // INC-024: usuario del ERP que recibe los avisos
  React.useEffect(() => { API.get('/api/programacion/usuarios').then(setUsuarios).catch(() => {}); }, []);
  const [dias, setDias] = React.useState([1, 2, 3, 4, 5]);
  const [aus, setAus] = React.useState([]);
  const [nuevaAus, setNuevaAus] = React.useState({ instalador_id: '', desde: '', hasta: '', motivo: '' });
  const nuevoRef = React.useRef(null); // GUT-259: botón "Nuevo instalador" lleva al campo de alta
  const irANuevo = () => { const el = nuevoRef.current; if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); el.focus(); } };
  const load = React.useCallback(() => {
    API.get('/api/programacion/instaladores').then(setInst).catch((e) => onToast(e.message, true));
    API.get('/api/programacion/festivos').then(setFest).catch((e) => onToast(e.message, true));
    API.get('/api/programacion/ajustes').then((a) => setDias(a.dias_laborables)).catch((e) => onToast(e.message, true));
    API.get('/api/programacion/ausencias').then(setAus).catch((e) => onToast(e.message, true));
  }, []);
  React.useEffect(() => { load(); }, [load, tick]);
  const done = () => { load(); refresh(); };
  const patch = (id, body) => API.patch(`/api/programacion/instaladores/${id}`, body).then(done).catch((e) => onToast(e.message, true));
  const addInst = () => API.post('/api/programacion/instaladores', { nombre: nuevoInst.trim().toUpperCase(), color: '#1f6e92' }).then(() => { setNuevoInst(''); done(); onToast('Instalador creado ✓'); }).catch((e) => onToast(e.message, true));
  const delInst = (i) => confirm(`¿Eliminar a ${i.nombre}? Los proyectos asignados quedarán sin instalador. Si solo quieres que no salga, desmarca "Activo".`) && API.del(`/api/programacion/instaladores/${i.id}`).then(done).catch((e) => onToast(e.message, true));
  const addFest = () => API.post('/api/programacion/festivos', nuevoFest).then(() => { setNuevoFest({ fecha: '', nombre: '' }); done(); onToast('Festivo añadido ✓'); }).catch((e) => onToast(e.message, true));
  const delFest = (f) => API.del(`/api/programacion/festivos/${f.fecha}`).then(done).catch((e) => onToast(e.message, true));
  const toggleDia = (n) => {
    const next = dias.includes(n) ? dias.filter((x) => x !== n) : [...dias, n];
    if (!next.length) return onToast('Debe quedar al menos un día laborable', true);
    API.put('/api/programacion/ajustes', { dias_laborables: next }).then((a) => { setDias(a.dias_laborables); refresh(); }).catch((e) => onToast(e.message, true));
  };
  const addAus = () => API.post('/api/programacion/ausencias', nuevaAus).then(() => { setNuevaAus({ instalador_id: '', desde: '', hasta: '', motivo: '' }); done(); onToast('Ausencia añadida ✓'); }).catch((e) => onToast(e.message, true));
  const delAus = (a) => API.del(`/api/programacion/ausencias/${a.id}`).then(done).catch((e) => onToast(e.message, true));
  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 2fr) minmax(0, 1fr)', gap: 20, alignItems: 'start' }}>
      <div>
        <div className="row-flex" style={{ justifyContent: 'space-between', alignItems: 'center', gap: 8 }}>
          <div className="section-title" style={{ flex: 1 }}>Personas: instaladores, medidores y operarios de fábrica</div>
          <button className="btn primary sm" onClick={irANuevo}><Icon name="plus" /> Nuevo instalador</button>
        </div>
        <div className="muted" style={{ fontSize: 11, margin: '4px 0 8px' }}>Esta lista es la que sale al elegir instaladores, medidor y operarios en Programación. No es la de Sistema › Operarios (código y PIN de la app de partes). Con «Usuario ERP» la persona recibe en la campana las tareas que se le asignen.</div>
        <div className="tbl-wrap"><div className="tbl-scroll" style={{ maxHeight: 'none' }}><table className="tbl tbl-compact pg-tbl">
          <thead><tr><th style={{ width: 40 }}>Color</th><th style={{ width: 130 }}>Nombre</th><th style={{ width: 120 }}>Tipo</th><th style={{ width: 130 }}>Contacto</th><th style={{ width: 100 }}>Ubicación</th><th style={{ width: 60 }}>H/día</th><th style={{ width: 60 }}>€/h</th><th style={{ width: 60 }}>Orden</th><th style={{ width: 55 }}>Activo</th><th style={{ width: 110 }} title="Usuario del ERP que recibe en la campana los avisos de las tareas que se le asignan">Usuario ERP</th><th>Observación</th><th /></tr></thead>
          <tbody>
            {inst.map((i) => (
              <tr key={i.id} className={`pg-row ${i.activo ? '' : 'pg-done'}`}>
                <td><input type="color" className="pg-color" value={i.color || '#1f6e92'} onChange={(e) => patch(i.id, { color: e.target.value })} /></td>
                <td className="strong"><PgEdit value={i.nombre} onSave={(v) => v && patch(i.id, { nombre: v.toUpperCase() })} /></td>
                <td><PgEdit type="select" value={i.tipo} options={[['nomina', 'Nómina'], ['subcontrata', 'Subcontrata'], ['equipo', 'Equipo']]} onSave={(v) => v && patch(i.id, { tipo: v })} /></td>
                <td><PgEdit value={i.contacto} onSave={(v) => patch(i.id, { contacto: v })} /></td>
                <td><PgEdit value={i.ubicacion} onSave={(v) => patch(i.id, { ubicacion: v })} /></td>
                <td><PgEdit type="num" value={i.horas_diarias} onSave={(v) => patch(i.id, { horas_diarias: v })} /></td>
                <td><PgEdit type="num" value={i.eur_hora} onSave={(v) => patch(i.id, { eur_hora: v })} /></td>
                <td><PgEdit type="num" value={i.orden} onSave={(v) => patch(i.id, { orden: v || 0 })} /></td>
                <td style={{ textAlign: 'center' }}><PgEdit type="check" value={i.activo} onSave={(v) => patch(i.id, { activo: v })} /></td>
                <td><PgEdit type="select" value={i.usuario_id} title="Recibe en la campana las tareas que se le asignen" options={usuarios.map((u) => [u.id, u.username])} onSave={(v) => patch(i.id, { usuario_id: v })} /></td>
                <td><PgEdit value={i.observacion} onSave={(v) => patch(i.id, { observacion: v })} /></td>
                <td><button className="icon-btn" onClick={() => delInst(i)}><Icon name="trash" size={12} /></button></td>
              </tr>
            ))}
          </tbody>
        </table></div></div>
        <div className="pg-addrow" style={{ marginTop: 10 }}>
          <input ref={nuevoRef} placeholder="Nombre del nuevo instalador o equipo (p. ej. PEPE/CHICO) y pulsa Añadir" value={nuevoInst} onChange={(e) => setNuevoInst(e.target.value)} style={{ flex: 1 }} onKeyDown={(e) => e.key === 'Enter' && nuevoInst.trim() && addInst()} />
          <button className="btn primary sm" onClick={addInst} disabled={!nuevoInst.trim()}><Icon name="plus" /> Añadir</button>
        </div>
        <div className="muted" style={{ fontSize: 11, marginTop: 8 }}>Para dar de baja a alguien, desmarca "Activo": deja de salir en los desplegables y en el calendario, pero conserva su historial.</div>

        <PgTiposTareaConfig onToast={onToast} />

        <div className="section-title" style={{ marginTop: 24 }}>Ausencias (vacaciones, bajas…)</div>
        <div className="tbl-wrap"><div className="tbl-scroll" style={{ maxHeight: 'none' }}><table className="tbl tbl-compact">
          <thead><tr><th>Instalador</th><th style={{ width: 110 }}>Desde</th><th style={{ width: 110 }}>Hasta</th><th>Motivo</th><th /></tr></thead>
          <tbody>
            {aus.length === 0 && <tr><td colSpan={5} className="muted" style={{ textAlign: 'center' }}>Sin ausencias</td></tr>}
            {aus.map((a) => <tr key={a.id} style={{ cursor: 'default' }}><td className="strong">{a.instalador_nombre}</td><td className="num">{pgFmtLong(a.desde)}</td><td className="num">{pgFmtLong(a.hasta)}</td><td>{a.motivo || '—'}</td><td><button className="icon-btn" onClick={() => delAus(a)}><Icon name="trash" size={12} /></button></td></tr>)}
          </tbody>
        </table></div></div>
        <div className="pg-addrow" style={{ marginTop: 10 }}>
          <select value={nuevaAus.instalador_id} onChange={(e) => setNuevaAus((x) => ({ ...x, instalador_id: e.target.value }))}>
            <option value="">Instalador…</option>
            {inst.filter((i) => i.activo).map((i) => <option key={i.id} value={i.id}>{i.nombre}</option>)}
          </select>
          <input type="date" value={nuevaAus.desde} onChange={(e) => setNuevaAus((x) => ({ ...x, desde: e.target.value, hasta: x.hasta || e.target.value }))} title="Desde" />
          <input type="date" value={nuevaAus.hasta} min={nuevaAus.desde} onChange={(e) => setNuevaAus((x) => ({ ...x, hasta: e.target.value }))} title="Hasta" />
          <input placeholder="Motivo (vacaciones, baja…)" value={nuevaAus.motivo} onChange={(e) => setNuevaAus((x) => ({ ...x, motivo: e.target.value }))} style={{ flex: 1 }} onKeyDown={(e) => e.key === 'Enter' && nuevaAus.instalador_id && nuevaAus.desde && addAus()} />
          <button className="btn primary sm" onClick={addAus} disabled={!nuevaAus.instalador_id || !nuevaAus.desde}><Icon name="plus" /> Añadir</button>
        </div>
        <div className="muted" style={{ fontSize: 11, marginTop: 8 }}>Las ausencias restan de los días disponibles en Carga y se marcan en el calendario semanal.</div>
      </div>
      <div>
        <div className="section-title">Días laborables</div>
        <div className="pg-dias">
          {PG_DIAS_SEMANA.map(([n, l]) => (
            <label key={n} className={`pg-dia ${dias.includes(n) ? 'on' : ''}`}>
              <input type="checkbox" checked={dias.includes(n)} onChange={() => toggleDia(n)} /> {l}
            </label>
          ))}
        </div>
        <div className="muted" style={{ fontSize: 11, marginTop: 8, marginBottom: 20 }}>Días de la semana que cuentan para la fecha fin de los trabajos y para los días disponibles en Carga.</div>

        <div className="section-title">Festivos</div>
        <div className="tbl-wrap"><div className="tbl-scroll" style={{ maxHeight: 'none' }}><table className="tbl tbl-compact">
          <thead><tr><th style={{ width: 100 }}>Fecha</th><th>Festivo</th><th /></tr></thead>
          <tbody>
            {fest.length === 0 && <tr><td colSpan={3} className="muted" style={{ textAlign: 'center' }}>Sin festivos</td></tr>}
            {fest.map((f) => <tr key={f.fecha} style={{ cursor: 'default' }}><td className="num">{pgFmtLong(f.fecha)}</td><td>{f.nombre || '—'}</td><td><button className="icon-btn" onClick={() => delFest(f)}><Icon name="trash" size={12} /></button></td></tr>)}
          </tbody>
        </table></div></div>
        <div className="pg-addrow" style={{ marginTop: 10 }}>
          <input type="date" value={nuevoFest.fecha} onChange={(e) => setNuevoFest((x) => ({ ...x, fecha: e.target.value }))} />
          <input placeholder="Nombre" value={nuevoFest.nombre} onChange={(e) => setNuevoFest((x) => ({ ...x, nombre: e.target.value }))} style={{ flex: 1 }} />
          <button className="btn primary sm" onClick={addFest} disabled={!nuevoFest.fecha}><Icon name="plus" /> Añadir</button>
        </div>
        <div className="muted" style={{ fontSize: 11, marginTop: 8 }}>Los festivos no cuentan como días laborables al calcular la fecha fin ni la carga.</div>
      </div>
    </div>
  );
};

window.ProgramacionView = ProgramacionView;
