Propuesta IA: cerrar los hallazgos medios de la auditoria

El mas urgente lo cause yo al hacer que el bloqueo bloqueara de verdad: si un
aviso bloqueante caia en un campo que el panel no dejaba editar, el asesor
quedaba sin salida salvo regenerar. Ocho campos eran editables y bastantes mas
se imprimen al cliente. Ahora se pueden editar tambien la cita destacada y su
autor, las dimensiones del valor y su nota de metodologia, los resultados con su
metrica y periodo, los materiales, las decisiones con quien decide, y el backlog.
El boton de descarga del documento del cliente aparece deshabilitado cuando hay
bloqueantes, en vez de invitar a un clic que devuelve 409.

Doble propuesta: se rechaza generar. El documento asume UNA lista de alcance y
UNA caja de totales, asi que sumaria las dos opciones —que son alternativas
excluyentes— y mostraria un total que no existe. Soportarlas es rediseniar el
documento; mientras tanto es mejor no producir uno incorrecto. Hoy no hay
cotizaciones dobles en produccion, asi que no bloquea a nadie.

La transcripcion ahora se guarda con la propuesta (columna nueva, migracion
aditiva). Solo vivia en memoria durante la generacion, asi que al editar y
revalidar, R3 se quedaba sin fuente contra la cual comprobar que la cita
destacada siguiera siendo literal, y el aviso desaparecia solo.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
urieljareth
2026-07-28 21:15:03 -06:00
co-authored by Claude Opus 5
parent 434a427f70
commit 0c127b44c4
4 changed files with 208 additions and 8 deletions
+182 -7
View File
@@ -172,6 +172,7 @@ export default function PropuestaIAPanel({ cotizacionId }: { cotizacionId: strin
const bloqueantes = avisos.filter((a) => a.severidad === "bloqueante");
const advertencias = avisos.filter((a) => a.severidad === "advertencia");
const citaDestacada = propuesta?.hechos.citas.find((c) => c.id === propuesta.redaccion.citaDestacadaId);
if (cargando) {
return (
@@ -371,6 +372,167 @@ export default function PropuestaIAPanel({ cotizacionId }: { cotizacionId: strin
))}
</div>
</div>
{/* Los grupos de abajo tambien se imprimen al cliente, asi que tambien
pueden disparar un aviso bloqueante. Si no fueran editables, un
bloqueante ahi no tendria mas salida que regenerar la propuesta. */}
{citaDestacada && (
<div>
<label className="block text-xs font-semibold uppercase tracking-wide text-muted mb-1">
Cita destacada
</label>
<p className="text-xs text-muted mb-2">
Se imprime literal. Editala solo para quitar un dato sensible, nunca para
mejorarle la redaccion al cliente.
</p>
<textarea
value={citaDestacada.textoLiteral}
onChange={(e) =>
editar((p) => {
const c = p.hechos.citas.find((x) => x.id === p.redaccion.citaDestacadaId);
if (c) c.textoLiteral = e.target.value;
})
}
rows={2}
className={`${INPUT} mb-2`}
/>
<input
value={citaDestacada.quienLoDijo}
onChange={(e) =>
editar((p) => {
const c = p.hechos.citas.find((x) => x.id === p.redaccion.citaDestacadaId);
if (c) c.quienLoDijo = e.target.value;
})
}
placeholder="Quien lo dijo (usa el rol, no el nombre)"
className={INPUT}
/>
</div>
)}
{propuesta.diagnostico.valorProblema.dimensiones.length > 0 && (
<div>
<label className="block text-xs font-semibold uppercase tracking-wide text-muted mb-2">
Lo que cuesta no resolverlo
</label>
<div className="space-y-2">
{propuesta.diagnostico.valorProblema.dimensiones.map((dim, i) => (
<div key={i} className="flex gap-2 items-start">
<span className="text-xs text-muted mt-2.5 shrink-0 w-20">{dim.tipo}</span>
<textarea
value={dim.descripcion}
onChange={(e) => editar((p) => { p.diagnostico.valorProblema.dimensiones[i].descripcion = e.target.value; })}
rows={2}
className={INPUT}
/>
</div>
))}
</div>
<textarea
value={propuesta.diagnostico.valorProblema.notaMetodologia}
onChange={(e) => editar((p) => { p.diagnostico.valorProblema.notaMetodologia = e.target.value; })}
rows={2}
placeholder="Nota de metodologia"
className={`${INPUT} mt-2`}
/>
</div>
)}
{propuesta.diagnostico.resultados.length > 0 && (
<div>
<label className="block text-xs font-semibold uppercase tracking-wide text-muted mb-2">
Resultados a lograr
</label>
<div className="space-y-2">
{propuesta.diagnostico.resultados.map((r, i) => (
<div key={i} className="space-y-1">
<textarea
value={r.enunciado}
onChange={(e) => editar((p) => { p.diagnostico.resultados[i].enunciado = e.target.value; })}
rows={2}
className={INPUT}
/>
<div className="flex gap-2">
<input
value={r.metrica}
onChange={(e) => editar((p) => { p.diagnostico.resultados[i].metrica = e.target.value; })}
placeholder="Metrica"
className={INPUT}
/>
<input
value={r.periodoMedicion}
onChange={(e) => editar((p) => { p.diagnostico.resultados[i].periodoMedicion = e.target.value; })}
placeholder="Periodo"
className={INPUT}
/>
</div>
</div>
))}
</div>
</div>
)}
{(propuesta.hechos.materialesPendientes.length > 0 ||
propuesta.hechos.decisionesPendientes.length > 0) && (
<div>
<label className="block text-xs font-semibold uppercase tracking-wide text-muted mb-2">
Que necesitamos de ustedes
</label>
<div className="space-y-2">
{propuesta.hechos.materialesPendientes.map((m, i) => (
<input
key={`mat-${i}`}
value={m.texto}
onChange={(e) => editar((p) => { p.hechos.materialesPendientes[i].texto = e.target.value; })}
className={INPUT}
/>
))}
{propuesta.hechos.decisionesPendientes.map((dd, i) => (
<div key={`dec-${i}`} className="flex gap-2">
<textarea
value={dd.texto}
onChange={(e) => editar((p) => { p.hechos.decisionesPendientes[i].texto = e.target.value; })}
rows={1}
className={INPUT}
/>
<input
value={dd.quienDecide}
onChange={(e) => editar((p) => { p.hechos.decisionesPendientes[i].quienDecide = e.target.value; })}
placeholder="Quien decide"
className={`${INPUT} max-w-[11rem] shrink-0`}
/>
</div>
))}
</div>
</div>
)}
{propuesta.redaccion.backlogEvolucion.length > 0 && (
<div>
<label className="block text-xs font-semibold uppercase tracking-wide text-muted mb-2">
Registrado para mas adelante
</label>
<div className="space-y-2">
{propuesta.redaccion.backlogEvolucion.map((b, i) => (
<div key={i} className="flex gap-2">
<textarea
value={b.problema}
onChange={(e) => editar((p) => { p.redaccion.backlogEvolucion[i].problema = e.target.value; })}
rows={1}
className={INPUT}
/>
<input
value={b.momentoSugerido}
onChange={(e) => editar((p) => { p.redaccion.backlogEvolucion[i].momentoSugerido = e.target.value; })}
placeholder="Cuando"
className={`${INPUT} max-w-[11rem] shrink-0`}
/>
</div>
))}
</div>
</div>
)}
</div>
<div className="flex flex-wrap items-center gap-2 pt-2 border-t border-border">
@@ -383,13 +545,26 @@ export default function PropuestaIAPanel({ cotizacionId }: { cotizacionId: strin
{guardando ? "Guardando..." : sucio ? "Guardar cambios" : "Sin cambios"}
</button>
<a
href={`/api/propuesta-ia/${cotizacionId}/pdf`}
className="flex items-center gap-2 px-4 py-2 border border-border rounded-lg text-sm hover:bg-gray-50"
>
<Download className="w-4 h-4" />
Propuesta consultiva (PDF)
</a>
{/* El servidor devuelve 409 si hay bloqueantes; aqui se refleja para no
invitar a un clic que va a fallar. La descarga del anexo interno
sigue disponible: es la que ayuda a entender que corregir. */}
{bloqueantes.length > 0 ? (
<span
className="flex items-center gap-2 px-4 py-2 border border-border rounded-lg text-sm text-muted bg-gray-50 cursor-not-allowed"
title="Resuelve los avisos bloqueantes y guarda para habilitar la descarga"
>
<Download className="w-4 h-4" />
Propuesta consultiva (PDF) bloqueada
</span>
) : (
<a
href={`/api/propuesta-ia/${cotizacionId}/pdf`}
className="flex items-center gap-2 px-4 py-2 border border-border rounded-lg text-sm hover:bg-gray-50"
>
<Download className="w-4 h-4" />
Propuesta consultiva (PDF)
</a>
)}
<a
href={`/api/propuesta-ia/${cotizacionId}/pdf?anexo=1`}