// =========================================================================
// Proyectos — servicios a terceros (interiorismo, intermediación, reforma
// externa, optimización). NO es cartera FLIP propia (eso son los inmuebles).
// Lee de la tabla `proyectos` de Supabase (mapeada en supabase-client.jsx).
// =========================================================================

const TIPO_TONE = {
  flip: "brand", intermediacion: "info", reforma: "warning",
  interiorismo: "info", optimizacion: "neutral", otro: "neutral",
};
const ESTADO_TONE = {
  aprobado: "info", presupuestando: "warning", en_curso: "info",
  finalizado: "success", cancelado: "danger", otro: "neutral",
};
const _label = (s) => (s || "—").replace(/_/g, " ").toUpperCase();
// "2026-03-04" → "04/03/2026". Sin new Date(): la fecha contable es un `date`
// de Postgres, no un instante, y parsearla como tal la desplaza un día según
// zona horaria.
const _fecha = (f) => {
  const s = String(f || "").slice(0, 10).split("-");
  return s.length === 3 ? `${s[2]}/${s[1]}/${s[0]}` : (f || "—");
};

// Balance con su tono: verde si >= 0, rojo si negativo.
// Balance con su color: verde si suma, rojo óxido si resta, neutro si es cero
// (cero no es ni bueno ni malo — antes caía del lado verde por un `< 0`).
const BalanceCell = ({ value }) => {
  const { fmtEUR } = window;
  if (value == null) return <span>—</span>;
  return <span className={"mono fin-amt " + window.finBalCls(value)}>{fmtEUR(value)}</span>;
};

// ------------------------------------------------------------------ LISTA
const ProjectsView = ({ onOpenProject, onCreate }) => {
  const { Icons, DATA, PageHeader, Card, Pill, fmtEUR } = window;
  const all = DATA.PROYECTOS || [];
  const [filter, setFilter] = React.useState("todos");

  const tipos = React.useMemo(() => {
    const counts = {};
    all.forEach(p => { counts[p.tipo] = (counts[p.tipo] || 0) + 1; });
    return Object.entries(counts).sort((a, b) => b[1] - a[1]);
  }, [all]);

  const rows = filter === "todos" ? all : all.filter(p => p.tipo === filter);

  return (
    <>
      <PageHeader
        eyebrow={`${all.length} proyectos · servicios a terceros`}
        title="PROYECTOS"
        sub="Interiorismo, intermediación, reforma externa y optimización. Distinto de la cartera FLIP propia (Inmuebles)."
        actions={onCreate ? <button className="btn primary" onClick={onCreate}><Icons.Plus/> Nuevo proyecto</button> : null}
      />
      <div className="fbar">
        <button className={"fchip" + (filter === "todos" ? " active" : "")} onClick={() => setFilter("todos")}>
          Todos · {all.length}
        </button>
        {tipos.map(([t, n]) => (
          <button key={t} className={"fchip" + (filter === t ? " active" : "")} onClick={() => setFilter(t)}>
            {_label(t)} · {n}
          </button>
        ))}
      </div>

      <Card title={`Proyectos · ${rows.length}`} b={false}>
        <table className="tbl">
          <thead>
            <tr>
              <th>Proyecto</th><th>Tipo</th><th>Estado</th><th>Cliente</th>
              <th className="num">Presupuesto</th><th className="num">Gastos</th>
              <th className="num">Balance</th><th>Inmueble</th>
            </tr>
          </thead>
          <tbody>
            {rows.map(p => (
              <tr key={p.id} style={{ cursor: "pointer" }} onClick={() => onOpenProject(p.id)}>
                <td style={{ fontWeight: 700 }}>{p.nombre}</td>
                <td><Pill tone={TIPO_TONE[p.tipo] || "neutral"}>{_label(p.tipo)}</Pill></td>
                <td><Pill tone={ESTADO_TONE[p.estado] || "neutral"}>{_label(p.estado)}</Pill></td>
                <td>{p.cliente || "—"}</td>
                <td className="num mono">{fmtEUR(p.presupuesto)}</td>
                <td className={"num mono " + (p.gastos == null ? "" : "fin-amt fin-out")}>{p.gastos == null ? "—" : fmtEUR(Math.abs(p.gastos))}</td>
                <td className="num"><BalanceCell value={p.balance} /></td>
                <td>{p.inmuebleUiId ? <Pill tone="neutral">VINCULADO</Pill> : "—"}</td>
              </tr>
            ))}
            {rows.length === 0 && (
              <tr><td colSpan={8} style={{ textAlign: "center", color: "var(--fg-3)", padding: 24 }}>Sin proyectos en este filtro.</td></tr>
            )}
          </tbody>
        </table>
      </Card>
    </>
  );
};

// -------------------------------------------------- Parseo de check_list_docs
// El texto viene en markdown ligero: líneas "- [ ] x" son ítems, el resto son
// encabezados de sección.
function parseChecklist(txt) {
  const out = [];
  (txt || "").split(/\r?\n/).forEach(line => {
    const t = line.trim();
    if (!t) return;
    const m = t.match(/^- \[( |x|X)\]\s*(.*)$/);
    if (m) out.push({ kind: "item", checked: m[1].toLowerCase() === "x", text: m[2] });
    else out.push({ kind: "head", text: t });
  });
  return out;
}

// ==================================================== COMISIÓN DE EQUIPO
// Fases 4 y 5 del MOTOR DE COMISIONES. Es el TIPO 2 de los tres (CLAUDE.md §4):
// REVIN paga a un comercial propio → GASTO con beneficiario_tipo='equipo'.
//
// TRES INVARIANTES QUE ESTA UI TIENE QUE RESPETAR:
//
// 1. UNA PREVISIÓN NO ES UN GASTO. Mientras el estado sea prevista/aprobada no
//    existe movimiento contable y no computa en ningún balance. Se pinta en
//    NEUTRO con "NO COMPUTA EN BALANCE", igual que el Plano 1.
//
// 2. LA BASE ES UN ACUERDO, NO UNA DERIVADA. El motor propone (suma de los
//    honorarios Tipo 1 de la operación) pero el usuario manda. Si toca la base,
//    se guarda base_origen='manual' para dejar constancia. Caso real que lo
//    motiva: Llana de San Juan 41, "30% de 4.500 €" cuando el sistema suma 4.920.
//
// 3. LOS CASOS VACÍOS SE DICEN EN NEUTRO, NUNCA EN ROJO NI CON UN 0 € ENGAÑOSO.
//    Venta directa sin comercial = 0 comisiones, y eso NO es un error. Proyecto
//    interno (naturaleza='interno') tampoco devenga comisión de intermediación,
//    pero no se bloquea: se ofrece "Añadir comisión" por si hay excepción.
//
// RECARGA SIN REMONTAR: se avisa al padre (`onSaved`) en vez de llamar a
// `window.RevinData.reload()`. Ese reload sube `dataVersion` y App.jsx remonta
// el `view-stage` entero (key={dataVersion}), que resetea la navegación y te
// saca de la ficha justo después de guardar. El mensaje de resultado vive en el
// padre (`flash`) porque al guardar bien esta tarjeta se remonta para releer los
// datos frescos, y un estado local se perdería en ese remonte.
const ComisionEquipoCard = ({ p, flash, onSaved, onError }) => {
  const { Icons, DATA, Card, Pill, fmtEUR } = window;
  const RS = window.RevinSupabase;
  const live = !!(window.RevinData && window.RevinData.isLive);

  // La vista `proyecto_comisiones_equipo` une DOS realidades distintas y la UI
  // no puede confundirlas (lo hacía: Murillo 26 — Gestión alquiler tiene una
  // comisión histórica de 165 € y la tarjeta la tomaba por una previsión pagada,
  // bloqueando la edición y enseñando la base de la propuesta como si fuera suya):
  //
  //   · PREVISIÓN VIVA  → nació en el motor (comisionId != null) y aún no es gasto.
  //     Es la única editable, y solo puede haber una abierta a la vez.
  //   · YA MATERIALIZADA → gasto real. O una previsión que se pagó, o uno de los
  //     14 movimientos históricos que nunca pasaron por el motor (comisionId null).
  //     Se muestra en solo lectura: reescribirla sería falsear contabilidad.
  const comisiones = (DATA.COMISIONES_EQUIPO || []).filter(c => c.proyectoId === p.id);
  const comision = comisiones.find(c => c.comisionId && c.estado !== "pagada") || null;
  const pagadas = comisiones.filter(c => c.estado === "pagada");
  // Sugerencia del motor para esta operación.
  const propuesta = (DATA.PROPUESTAS_COMISION || []).find(x => x.proyectoId === p.id) || null;

  // ---- ANTI-DUPLICADO (bug del doble pago, 22/07/2026) -------------------
  // Murillo 26 — Gestión alquiler acabó con DOS gastos de 165 € a Hiurma: la
  // comisión real importada de Notion (04/03) y otra creada por el motor. La
  // tarjeta ya ENSEÑABA la histórica justo encima del formulario que la
  // duplicaba, pero precargaba la base de la propuesta e invitaba a pagar.
  //
  // `yaRegistrada` viene de la vista (ampliada en el 27_ para mirar también los
  // movimientos Tipo 2 históricos) y cubre un caso que `pagadas` no ve: la
  // comisión que cuelga del INMUEBLE y no del proyecto — las 11 del backfill.
  const yaRegistrada = !!(propuesta && propuesta.yaRegistrada);
  const pagadaPrevia = pagadas.slice()
    .sort((a, b) => String(b.fecha || "").localeCompare(String(a.fecha || "")))[0] || null;
  const previaImporte = pagadaPrevia ? pagadaPrevia.importe : (propuesta ? propuesta.comisionPagadaImporte : null);
  const previaFecha   = pagadaPrevia ? pagadaPrevia.fecha   : (propuesta ? propuesta.comisionPagadaFecha : null);
  // Sin previsión viva pero con comisión ya pagada: crear otra sería duplicar.
  const duplicaria = !comision && (pagadas.length > 0 || yaRegistrada);

  // Candidatos a responsable: todo el equipo con ficha en `usuarios`, admins
  // incluidos — Carlos cierra operaciones y su comisión no se reasigna a nadie.
  const equipo = (DATA.TEAM || []).filter(t => t.supabase && t.supabase.id && t.role !== "Cliente");

  const esInterno = p.naturaleza === "interno";

  const [comercialId, setComercialId] = React.useState(p.comercialId || "");
  // La base NO se precarga si la operación ya tiene comisión pagada: un campo
  // relleno con 550 € es una invitación a pulsar "Guardar previsión" y repartir
  // por segunda vez unos honorarios ya repartidos. Que haya que teclearla es el
  // punto — obliga a un acto consciente.
  const [base, setBase] = React.useState(
    comision && comision.base != null ? String(comision.base)
      : (propuesta && propuesta.baseSugerida != null && !duplicaria ? String(propuesta.baseSugerida) : "")
  );
  const [pct, setPct] = React.useState(
    comision && comision.pct != null ? String(comision.pct)
      : (p.comercialPct != null ? String(p.comercialPct) : "")
  );
  const [forzarInterno, setForzarInterno] = React.useState(false);
  const [busy, setBusy] = React.useState(null);   // qué acción está en curso
  // Materializar es el único clic que toca la contabilidad: se confirma antes.
  const [confirmarPago, setConfirmarPago] = React.useState(false);
  // Se arma SOLO cuando el servidor devuelve 23505 (anti-duplicado del 27_).
  const [dupError, setDupError] = React.useState(null);

  // Al cambiar de comercial, el % sigue al de su ficha MIENTRAS no haya
  // previsión guardada. Si ya la hay, el % guardado es un acuerdo cerrado y no
  // se pisa solo.
  const cambiarComercial = (id) => {
    setComercialId(id);
    if (comision) return;
    const u = equipo.find(t => t.supabase.id === id);
    setPct(u && u.supabase.comision_pct != null ? String(u.supabase.comision_pct) : "");
  };

  const baseNum = Number(base || 0);
  const pctNum = Number(pct || 0);
  const importe = Math.round(baseNum * pctNum) / 100;
  // ¿Se ha tocado la base respecto a lo que propone el motor? El `baseNum > 0`
  // de la segunda rama importa desde que la base no se precarga en operaciones
  // que ya tienen comisión: un campo vacío no es una base "pactada a mano", es
  // un campo vacío, y decir lo contrario ensuciaría `base_origen` al guardar.
  const baseTocada = !propuesta || propuesta.baseSugerida == null
    ? baseNum > 0
    : baseNum > 0 && Math.abs(baseNum - propuesta.baseSugerida) > 0.005;

  const comercialCambiado = (comercialId || null) !== (p.comercialId || null);
  const finalizado = p.estado === "finalizado";

  // En error NO se avisa a onSaved: la tarjeta no se remonta y el usuario
  // conserva lo que había tecleado para corregirlo.
  const run = async (nombre, fn) => {
    setBusy(nombre);
    try {
      const r = await fn();
      await onSaved(r);
    } catch (e) {
      console.error(e);
      onError(e.message || "Error al guardar");
      setBusy(null);
    }
  };

  const guardarComercial = () => run("comercial", async () => {
    const u = equipo.find(t => t.supabase.id === comercialId);
    await RS.asignarComercialProyecto(p.id, comercialId || null, u ? u.name : null);
    return comercialId ? `✓ ${u ? u.name : "Comercial"} asignado a la operación.` : "✓ Comercial desasignado.";
  });

  const guardarPrevision = () => run("prevision", async () => {
    await RS.guardarPrevisionComision({
      comisionId: comision ? comision.comisionId : null,
      proyectoId: p.id,
      comercialId: comercialId || p.comercialId,
      base: baseNum,
      pct: pctNum,
      // La constancia del punto 2: si la base no es la que propone el motor,
      // queda marcada como pactada a mano.
      baseOrigen: baseTocada ? "manual" : "calculada",
      baseMovimientoIds: propuesta ? propuesta.baseMovimientoIds : [],
    });
    return `✓ Previsión guardada: ${fmtEUR(importe)} (${pctNum}% de ${fmtEUR(baseNum)}).`;
  });

  const borrarPrevision = () => run("borrar", async () => {
    await RS.borrarPrevisionComision(comision.comisionId);
    return "✓ Previsión eliminada.";
  });

  // No usa `run`: necesita distinguir el rechazo del anti-duplicado (23505) de
  // un error cualquiera, porque ése no se descarta — se ofrece forzarlo.
  const pagar = async (forzando) => {
    setBusy("pagar");
    setDupError(null);
    try {
      const r = await RS.materializarComision(comision.comisionId, { forzar: !!forzando });
      setConfirmarPago(false);
      await onSaved(r && r.ya_materializada
        ? "Esta comisión ya estaba pagada. No se ha duplicado el gasto."
        : `✓ Comisión pagada: movimiento contable de ${fmtEUR(Number(r.importe))} creado y enlazado.`
          + (r && r.forzada ? " Forzada: queda constancia en las notas del movimiento." : ""));
    } catch (e) {
      console.error(e);
      if (e.duplicado) setDupError(e.message);
      else onError(e.message || "Error al guardar");
      setBusy(null);
    }
  };

  // ---- Estado vacío NEUTRO para proyectos internos -----------------------
  if (esInterno && !comision && pagadas.length === 0 && !forzarInterno) {
    return (
      <Card title="Comisión de equipo" right={<Pill tone="neutral">PROYECTO INTERNO</Pill>} b={false} style={{ marginTop: 20 }}>
        <div style={{ padding: 16, fontSize: 13, color: "var(--fg-2)", lineHeight: 1.6 }}>
          Proyecto interno · no devenga comisión de intermediación.
          <div style={{ fontSize: 12, color: "var(--fg-3)", marginTop: 4 }}>
            La naturaleza es un eje independiente del tipo: este proyecto no es una operación
            del negocio inmobiliario, así que no genera comisión al equipo.
          </div>
          <button className="btn ghost xs" style={{ marginTop: 10, paddingLeft: 0 }} onClick={() => setForzarInterno(true)}>
            <Icons.Plus/> Añadir comisión
          </button>
        </div>
      </Card>
    );
  }

  const estadoPill = !comision ? null
    : comision.estado === "aprobada"  ? <Pill tone="info">APROBADA</Pill>
    : comision.estado === "cancelada" ? <Pill tone="neutral">CANCELADA</Pill>
    : <Pill tone="neutral">PREVISIÓN</Pill>;

  return (
    <Card
      title="Comisión de equipo"
      right={<>
        {pagadas.length > 0 && <Pill tone="success">{pagadas.length} PAGADA{pagadas.length > 1 ? "S" : ""}</Pill>}
        {estadoPill}
        {comision && <Pill tone="neutral">NO COMPUTA EN BALANCE</Pill>}
      </>}
      b={false}
      style={{ marginTop: 20 }}
    >
      <div style={{ padding: "10px 16px", fontSize: 12, color: "var(--fg-3)", borderBottom: "1px solid var(--border-1)" }}>
        El % se aplica sobre los <b>honorarios que cobra REVIN</b> en la operación, no sobre el precio de venta.
        Una previsión no es un gasto: solo al marcarla pagada se crea el movimiento contable.
      </div>

      {/* ---- Comisiones YA MATERIALIZADAS · solo lectura ----
          Son gasto real y ya salen en los movimientos contables de abajo. No se
          editan desde aquí: corregir una comisión pagada es corregir un asiento,
          y eso se hace en Contabilidad. */}
      {pagadas.length > 0 && (
        <div style={{ padding: "12px 16px", borderBottom: "1px solid var(--border-1)" }}>
          <div className="field-lbl" style={{ marginBottom: 8 }}>Ya pagadas</div>
          {pagadas.map((c, i) => (
            <div key={c.comisionId || c.movimientoId || i}
                 style={{ display: "flex", gap: 10, alignItems: "center", fontSize: 13, padding: "4px 0" }}>
              {/* Ya materializada: es GASTO real y se pinta como tal. */}
              <span className="mono fin-amt fin-out" style={{ minWidth: 90 }}>{fmtEUR(c.importe)}</span>
              <span style={{ color: "var(--fg-2)" }}>{c.comercialNombre || "Sin comercial"}</span>
              {c.pct != null && <span style={{ color: "var(--fg-3)", fontSize: 12 }}>{c.pct}% de {fmtEUR(c.base)}</span>}
              {/* Los 14 movimientos migrados de Notion nunca pasaron por el motor:
                  no tienen base ni % que enseñar, y decirlo es más honesto que
                  inventar un 0 %. */}
              {!c.comisionId && <Pill tone="neutral">MOVIMIENTO HISTÓRICO</Pill>}
              {c.fecha && <span className="mono" style={{ marginLeft: "auto", fontSize: 11, color: "var(--fg-3)" }}>{c.fecha}</span>}
            </div>
          ))}
        </div>
      )}

      {/* ---- FASE 4.2 · comercial responsable ---- */}
      <div style={{ padding: 16, borderBottom: "1px solid var(--border-1)", display: "flex", gap: 12, alignItems: "flex-end" }}>
        <div className="field" style={{ flex: 1, maxWidth: 320 }}>
          <label className="field-lbl" htmlFor="cmp-comercial">Comercial responsable</label>
          <select id="cmp-comercial" className="select" value={comercialId}
                  onChange={(e) => cambiarComercial(e.target.value)} disabled={!live}>
            <option value="">— Sin comercial asignado —</option>
            {equipo.map(t => (
              <option key={t.supabase.id} value={t.supabase.id}>
                {t.name}{t.supabase.comision_pct != null ? ` · ${t.supabase.comision_pct}%` : ""}
                {t.estado === "inactivo" ? " (inactivo)" : ""}
              </option>
            ))}
          </select>
        </div>
        <button className="btn secondary" onClick={guardarComercial}
                disabled={!live || !comercialCambiado || busy === "comercial"}>
          {busy === "comercial" ? "Guardando…" : "Guardar responsable"}
        </button>
        {!p.comercialId && !comercialCambiado && (
          <div style={{ fontSize: 12, color: "var(--fg-3)", paddingBottom: 9 }}>
            Sin comercial asignado — venta directa. No genera comisión.
          </div>
        )}
      </div>

      {/* ---- AVISO ANTI-DUPLICADO ----
          En NEUTRO, no en rojo: no es un error del usuario ni un estado
          inválido, es información que necesita antes de teclear. Mismo criterio
          que "proyecto interno" y "venta directa sin comercial". Y nunca un
          bloqueo duro: dos comisiones al mismo comercial en una operación son
          posibles (alquiler renovado) — solo tienen que ser deliberadas. */}
      {duplicaria && (
        <div style={{ margin: "12px 16px 0", padding: "10px 12px", borderRadius: 4,
                      background: "var(--bg-surface-2)", borderLeft: "3px solid var(--warning)",
                      fontSize: 13, color: "var(--fg-2)", lineHeight: 1.6 }}>
          Esta operación <b>ya tiene una comisión pagada</b>
          {(pagadaPrevia && pagadaPrevia.comercialNombre) ? <> a {pagadaPrevia.comercialNombre}</> : null}
          {previaImporte != null ? <>: <b className="mono">{fmtEUR(previaImporte)}</b></> : null}
          {previaFecha ? <> el <span className="mono">{_fecha(previaFecha)}</span></> : null}.
          Crear otra duplicaría el gasto.
          <div style={{ fontSize: 12, color: "var(--fg-3)", marginTop: 4 }}>
            La base no se precarga a propósito. Si de verdad son dos comisiones distintas
            (p. ej. un alquiler renovado), tecléala y guarda: al marcarla pagada el sistema
            te pedirá confirmarlo expresamente.
          </div>
        </div>
      )}

      {/* ---- FASE 4.3 · previsión editable ---- */}
      <div style={{ padding: 16, display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 16, alignItems: "end" }}>
        <div className="field">
          <label className="field-lbl" htmlFor="cmp-base">Base · honorarios</label>
          <input id="cmp-base" className="input" type="number" min="0" step="0.01" value={base}
                 onChange={(e) => setBase(e.target.value)} disabled={!live} placeholder="0.00"/>
          <div className="field-hint">
            {propuesta && propuesta.baseSugerida != null
              ? <>Motor propone {fmtEUR(propuesta.baseSugerida)}{baseTocada ? " · editada a mano" : ""}</>
              : "Sin honorarios registrados en esta operación"}
          </div>
        </div>
        <div className="field">
          <label className="field-lbl" htmlFor="cmp-pct">% aplicado</label>
          <input id="cmp-pct" className="input" type="number" min="0" max="100" step="0.01" value={pct}
                 onChange={(e) => setPct(e.target.value)} disabled={!live} placeholder="0"/>
          <div className="field-hint">Override por operación · el % de ficha es solo la semilla</div>
        </div>
        <div className="field">
          <label className="field-lbl">Importe</label>
          {/* ÁMBAR, no rojo: una previsión NO es un gasto — no computa en
              ningún balance hasta materializarse. El color lo dice antes que
              la pill "NO COMPUTA EN BALANCE". */}
          <div className="mono fin-pend" style={{ fontFamily: "var(--font-display)", fontSize: 22, letterSpacing: ".02em" }}>
            {fmtEUR(importe)}
          </div>
          <div className="field-hint">
            {comision && comision.baseOrigen === "manual" ? "Base pactada a mano" : "Base calculada por el motor"}
          </div>
        </div>
      </div>

      {flash && (
        <div role="status" style={{ margin: "0 16px 12px", padding: 10, borderRadius: 4, fontSize: 13,
                      background: flash.error ? "var(--danger-bg)" : "var(--success-bg)",
                      color: flash.error ? "var(--danger-fg)" : "var(--success-fg)" }}>
          {flash.text}
        </div>
      )}

      <div className="card-foot" style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
        <button className="btn primary" onClick={guardarPrevision}
                disabled={!live || !(comercialId || p.comercialId) || busy === "prevision"}>
          {busy === "prevision" ? "Guardando…" : (comision ? "Guardar cambios" : "Guardar previsión")}
        </button>
        {comision && (
          <button className="btn ghost" onClick={borrarPrevision} disabled={!live || busy === "borrar"}>
            {busy === "borrar" ? "Borrando…" : "Eliminar previsión"}
          </button>
        )}

        {/* ---- FASE 5 · materialización ----
            Dos pasos. El primer clic no escribe nada: arma la confirmación. Es
            el único botón de la ficha que crea un GASTO REAL, y deshacerlo
            obliga a tocar contabilidad a mano. */}
        <div style={{ marginLeft: "auto", display: "flex", gap: 8, alignItems: "center" }}>
          {confirmarPago && (
            <span style={{ fontSize: 12, color: "var(--fg-2)" }}>
              ¿Crear el gasto de <b className="mono">{fmtEUR(comision ? comision.importe : importe)}</b>? Esto sí toca la contabilidad.
            </span>
          )}
          {confirmarPago && (
            <button className="btn ghost" onClick={() => { setConfirmarPago(false); setDupError(null); }}
                    disabled={busy === "pagar"}>Cancelar</button>
          )}
          <button className="btn dark" onClick={() => (confirmarPago ? pagar(false) : setConfirmarPago(true))}
                  disabled={!live || !comision || !finalizado || busy === "pagar"}
                  title={!comision ? "Guarda antes la previsión"
                        : !finalizado ? "El proyecto debe estar finalizado" : "Crea el movimiento contable"}>
            {busy === "pagar" ? "Pagando…"
              : confirmarPago ? <><Icons.Euro/> Sí, crear el gasto</>
              : <><Icons.Euro/> Marcar comisión pagada</>}
          </button>
        </div>
      </div>

      {/* ---- Rechazo del anti-duplicado (servidor, 23505) ----
          No se descarta como un error cualquiera: el servidor no sabe si es un
          duplicado o una segunda comisión legítima, así que expone el dato y
          deja decidir. Forzar deja constancia en las notas del movimiento. */}
      {dupError && (
        <div role="alert" style={{ margin: "0 16px 14px", padding: 12, borderRadius: 4,
                      background: "var(--bg-surface-2)", borderLeft: "3px solid var(--warning)",
                      fontSize: 13, color: "var(--fg-2)", lineHeight: 1.6 }}>
          {dupError}
          <div style={{ display: "flex", gap: 8, marginTop: 10 }}>
            <button className="btn ghost xs" onClick={() => setDupError(null)}>Dejarlo como está</button>
            <button className="btn secondary xs" onClick={() => pagar(true)} disabled={busy === "pagar"}>
              Son dos comisiones distintas · pagar igualmente
            </button>
          </div>
        </div>
      )}

      {/* Por qué el botón está apagado: se explica, no se deja adivinar. */}
      {(!comision || !finalizado) && (
        <div style={{ padding: "0 16px 14px", fontSize: 12, color: "var(--fg-3)" }}>
          {!comision
            ? "Guarda la previsión antes de poder marcarla como pagada."
            : <>El proyecto está <b>{_label(p.estado).toLowerCase()}</b>: la comisión se paga al cierre
               (estado <b>finalizado</b>), porque es entonces cuando REVIN cobra sus honorarios.</>}
        </div>
      )}
    </Card>
  );
};

// ------------------------------------------------------------------ FICHA
const ProjectDetail = ({ projectId, onBack, onOpenProperty }) => {
  const { Icons, DATA, PageHeader, Card, Pill, KPIRow, fmtEUR } = window;

  // Recarga local tras escribir (ver ComisionEquipoCard): sube `refresh`, que
  // remonta solo la tarjeta de comisión y re-renderiza esta ficha con el
  // `window.DATA` fresco, SIN tocar `dataVersion` (que remontaría toda la vista).
  const [refresh, setRefresh] = React.useState(0);
  const [flash, setFlash] = React.useState(null);
  const recargar = React.useCallback(async (texto) => {
    if (window.RevinSupabase && window.RevinData && window.RevinData.isLive) {
      window.DATA = await window.RevinSupabase.loadDataFromSupabase();
    }
    setFlash(texto ? { text: texto, error: false } : null);
    setRefresh(r => r + 1);
  }, []);

  const p = (DATA.PROYECTOS || []).find(x => x.id === projectId) || (DATA.PROYECTOS || [])[0];
  if (!p) return <div className="empty">Proyecto no encontrado.</div>;

  const checklist = p.checkListDocs ? parseChecklist(p.checkListDocs) : [];

  // Movimientos de este proyecto — balance CALCULADO desde la contabilidad,
  // sustituye a los campos manuales gastos_totales/balance heredados de Notion.
  const todosMovs = (DATA.MOVIMIENTOS || []).filter(m => m.proyectoId === p.id);
  const bal = window.movBalance ? window.movBalance(todosMovs) : { ingresos: 0, gastos: 0, balance: 0, balanceCobrado: 0, ingresosPendientes: 0, n: 0, operacion: [] };
  // LOS DOS PLANOS (CLAUDE.md §4): el Plano 1 se lista aparte y NO computa.
  const movs = todosMovs.filter(m => !m.esOperacion);   // Plano 2 · contable
  const movsOp = todosMovs.filter(m => m.esOperacion);  // Plano 1 · operación
  const gastosVal = movs.length ? bal.gastos : (p.gastos == null ? null : Math.abs(p.gastos));
  const balanceVal = movs.length ? bal.balance : p.balance;

  return (
    <>
      {window.DemoDataBanner && <window.DemoDataBanner/>}
      <div className="page-h">
        <div>
          <button className="btn ghost xs" onClick={onBack} style={{ marginBottom: 6, paddingLeft: 0 }}>
            <Icons.ChevronLeft /> Volver a proyectos
          </button>
          <h1 style={{ fontSize: 28 }}>{p.nombre}</h1>
          <div className="sub" style={{ display: "flex", gap: 8, alignItems: "center", marginTop: 6 }}>
            <Pill tone={TIPO_TONE[p.tipo] || "neutral"}>{_label(p.tipo)}</Pill>
            <Pill tone={ESTADO_TONE[p.estado] || "neutral"}>{_label(p.estado)}</Pill>
            {/* Naturaleza: eje independiente del tipo. Solo se pinta cuando es
                INTERNO — lo normal en una inmobiliaria es lo inmobiliario, y una
                pill que sale siempre deja de informar. */}
            {p.naturaleza === "interno" && <Pill tone="neutral">INTERNO</Pill>}
            {p.comercialIniciales && <Pill tone="info">{p.comercialIniciales}</Pill>}
            {p.cliente && <span style={{ color: "var(--fg-2)" }}>· {p.cliente}</span>}
          </div>
        </div>
        <div className="actions">
          {p.inmuebleUiId && (
            <button className="btn secondary" onClick={() => onOpenProperty(p.inmuebleUiId)}>
              <Icons.Building /> Ver inmueble
            </button>
          )}
        </div>
      </div>

      <KPIRow cols={5} items={[
        // El presupuesto no es ni ingreso ni gasto: es una previsión de obra.
        // Sin `fin` — color de texto normal.
        { label: "Presupuesto", value: fmtEUR(p.presupuesto) },
        { label: "Ingresos", value: fmtEUR(bal.ingresos), fin: "fin-in" },
        { label: "Gastos", value: gastosVal == null ? "—" : fmtEUR(gastosVal), fin: gastosVal == null ? null : "fin-out" },
        { label: "Balance", value: fmtEUR(balanceVal), fin: window.finBalCls(balanceVal),
          tone: (balanceVal != null && balanceVal < 0) ? "down" : "up",
          sub: bal.ingresosPendientes > 0 ? `devengo · ${fmtEUR(bal.ingresosPendientes)} sin cobrar` : "devengo" },
        { label: "Balance cobrado", value: movs.length ? fmtEUR(bal.balanceCobrado) : "—",
          fin: movs.length ? window.finBalCls(bal.balanceCobrado) : null,
          tone: (bal.balanceCobrado != null && bal.balanceCobrado < 0) ? "down" : "up",
          sub: "caja real · solo lo efectivamente cobrado" },
      ]} />

      {/* La tarjeta de comisión de equipo es de administración entera:
          - enseña la base de honorarios que cobró REVIN y lo que se llevó cada
            comercial en las ya pagadas, con nombre e importe;
          - y sus tres botones ("Guardar previsión", "Eliminar previsión",
            "Marcar comisión pagada") escriben en `comisiones_equipo` y en
            `movimientos`, que solo admite `is_admin()`. A un comercial le
            saldría el mensaje crudo de Postgres en mitad de la pantalla:
            `new row violates row-level security policy for table ...`.
          Un botón que no puede funcionar no se ofrece. */}
      {window.veLaCaja() && (
        <ComisionEquipoCard
          key={`comision-${p.id}-${refresh}`}
          p={p}
          flash={flash}
          onSaved={recargar}
          onError={(texto) => setFlash({ text: texto, error: true })}
        />
      )}

      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 20, marginTop: 20 }}>
        <Card title="Notas" b={false}>
          <div style={{ padding: 16, fontSize: 13, color: "var(--fg-2)", whiteSpace: "pre-wrap" }}>
            {p.notas || "Sin notas."}
          </div>
        </Card>

        <Card title="Checklist de documentación" right={checklist.length ? <Pill tone="neutral">{checklist.filter(c => c.kind === "item").length} ítems</Pill> : null} b={false}>
          <div style={{ padding: 16 }}>
            {checklist.length === 0 && <div style={{ fontSize: 13, color: "var(--fg-3)" }}>Sin checklist asociado.</div>}
            {checklist.map((c, i) => c.kind === "head" ? (
              <div key={i} style={{ fontSize: 12, fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.4, color: "var(--fg-1)", marginTop: i ? 14 : 0, marginBottom: 6 }}>{c.text}</div>
            ) : (
              <label key={i} style={{ display: "flex", gap: 8, alignItems: "flex-start", fontSize: 13, color: "var(--fg-2)", padding: "3px 0" }}>
                <input type="checkbox" checked={c.checked} readOnly style={{ marginTop: 2 }} />
                <span>{c.text}</span>
              </label>
            ))}
          </div>
        </Card>
      </div>

      {/* PLANO 1 — datos de la operación. Dinero entre terceros o mero dato
          documental: se registra por trazabilidad y NO computa en el balance. */}
      {movsOp.length > 0 && (
        <Card
          title={`Datos de la operación · ${movsOp.length}`}
          right={<Pill tone="neutral">NO COMPUTA EN BALANCE</Pill>}
          b={false}
          style={{ marginTop: 20 }}
        >
          <div style={{ padding: "10px 16px", fontSize: 12, color: "var(--fg-3)", borderBottom: "1px solid var(--border-1)" }}>
            Dinero que fluye entre las partes (comprador ↔ vendedor) o que solo se documenta.
            REVIN lo registra por transparencia; no es ingreso ni gasto suyo.
          </div>
          <window.MovimientosTable rows={movsOp} showVinculo={false} empty="Sin datos de operación."/>
        </Card>
      )}

      {window.veLaCaja() && (
      <Card
        title={`Movimientos contables · ${movs.length}`}
        right={<Pill tone="neutral">BALANCE CALCULADO</Pill>}
        b={false}
        style={{ marginTop: 20 }}
      >
        <window.MovimientosTable
          rows={movs}
          showVinculo={false}
          empty="Este proyecto todavía no tiene movimientos vinculados en contabilidad."
        />
      </Card>
      )}
    </>
  );
};

window.ProjectsView = ProjectsView;
window.ProjectDetail = ProjectDetail;
window.ComisionEquipoCard = ComisionEquipoCard;
