fix(auditoria general): 25 errores encontrados en la revision modulo por modulo

SEGURIDAD (agente IA):
- Conversaciones: GET /ai/conversations/:id y el reanudar chat verifican
  propiedad (antes cualquier usuario leia/continuaba la conversacion de otro).
- /ai/documents pasa a solo-admin (antes cualquien podia inyectar o borrar
  documentos de la base de conocimiento).

DATOS / CORRUPCION:
- Cotizador: elegir un cliente del maestro en el buscador ya no crea un lead
  fantasma "Registro previo (CRM anterior)" en el embudo.
- Afluencia con oportunidadId inexistente: 404 en vez de crear sombra fantasma.
- Cobranza: los 103 planes importados quedaron con vendedor asignado (7 con su
  asesor real segun el Excel, el resto al admin) y el motor de recordatorios
  cae al admin cuando un plan no tiene vendedor: antes los 411 recordatorios
  se registraban 'sin-destinatario' y no los veia nadie.
- Backups: script reescrito para el host real (sqlite .backup + uploads + env
  en /root/backups) y cron instalado (antes: script que asumia docker y sin
  cron programado = cero respaldos).

REPORTES / METRICAS:
- Mi tablero: el embudo ya excluye cancelaciones reales (Cancelo/Cancelacion
  Apartado) y cuenta como vendidos Firma Cliente/Escriturado/Titulado (antes
  41 "apartados" donde habia 13 activos y vendidos buscaba una etapa que no
  existe); tasa de descarte ya compara el mismo periodo.
- Scoring: etapa real 'Agendo Cita' (antes 'AGENDO' nunca aplicaba) y los
  descartados/perdidos ya no reciben score.
- ver_cobranza (IA): ignora planes cancelados y no cuenta como vencido el pago
  que vence hoy.

UI / FLUJOS:
- mi-dia: typo CANCO -> CANCELO (los cancelados reaparecen en Escrituracion);
  confirmar cita ya no retrocede la etapa de Cotizador/Aparto (boton y kanban).
- alta-clientes: la firma ya no se borra al navegar fuera del paso de firmas;
  toast de exito solo cuando el API confirma; prefill de apartado trae
  direccion, estado civil y fecha de nacimiento desde el alta.
- Auditoria: el filtro por usuario funciona (el front enviaba 'usuario' y el
  API esperaba 'usuarioId') y 'hasta' incluye todo el dia.
- Contactos: paginacion aplicada despues de fusionar fuentes (antes saltaban o
  se repetian contactos entre paginas y 'Siguiente' se deshabilitaba antes de
  tiempo).
- Buscador global: leads -> /hoy/, contactos -> /; el cotizador entiende
  ?cotizacion=<id> para retomar desde la liga.
- Export mi-dia: columnas Desarrollo/Origen rellenas y sin duplicados.
- Recibo de cobranza: "Recibio" es FRASA (el cliente paga, no recibe).
- Alta de plan de cobranza: proyecto libre con sugerencias del catalogo real
  (antes limitado a 3 proyectos que no cubrian la cartera).

Verificado en vivo: autorizacion IA (3x 403), filtro de auditoria, 404 de
afluencia. Suites: 717 API + 124 alta-clientes + 115 cotizador + 92 cobranza +
89 landing + 32 mi-dia en verde. Backup de hoy generado en /root/backups.
This commit is contained in:
2026-09-24 10:54:09 +00:00
parent cfc0acb5d3
commit 8a2584438d
30 changed files with 258 additions and 102 deletions

View File

@@ -209,7 +209,16 @@ function FilaOportunidad({
<button
type="button"
disabled={enviando}
onClick={() => (conCita ? onEtapa('Agendo Cita', fechaCita || undefined) : setConCita(true))}
onClick={() =>
conCita
? // Misma regla que el modal: si la op ya pasó por Agendo Cita
// (Cotizador/Aparto) NO se retrocede la etapa al confirmar.
onEtapa(
op.etapa === 'Cotizador' || op.etapa === 'Aparto' ? op.etapa : 'Agendo Cita',
fechaCita || undefined,
)
: setConCita(true)
}
className={CLASE_BOTON_ACCION}
>
{conCita ? 'Confirmar cita' : ETIQUETA_FASE_OPORTUNIDAD['Agendo Cita']}
@@ -1144,7 +1153,7 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
// marcarlo como Escriturado o Titulado
e.includes('ESCRITURADO') ||
e.includes('TITULADO') ||
e.includes('CANCO')
e.includes('CANCELO')
)
})
: []
@@ -1357,7 +1366,14 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
if (op) setDescartando({ id: op.id, nombre: op.nombre, fuente: 'oportunidad' })
return
}
void ejecutar(() => cambiarEtapaOportunidad(id, estado), 'No se pudo actualizar')
// No retroceder etapa al soltar en "Agendo Cita" una
// op que ya está en Cotizador/Aparto
const op = resumen.misOportunidades.items.find((o) => o.id === id)
const destino =
estado === 'Agendo Cita' && op && (op.etapa === 'Cotizador' || op.etapa === 'Aparto')
? op.etapa
: estado
void ejecutar(() => cambiarEtapaOportunidad(id, destino), 'No se pudo actualizar')
}}
/>
) : (

View File

@@ -27,7 +27,7 @@ export function exportarLeads(leads: LeadMiDia[], nombreArchivo?: string) {
Email: l.email,
Estado: l.estado,
Origen: l.origen,
Desarrollo: '',
Desarrollo: l.desarrollo,
'Último cambio': l.ultimoCambio ? `${l.ultimoCambio.detalle} · ${l.ultimoCambio.usuario ?? ''}` : '',
Fuente: l.fuente,
}))
@@ -43,12 +43,10 @@ export function exportarOportunidades(ops: OportunidadMiDia[], nombreArchivo?: s
Nombre: o.nombre,
Teléfono: o.telefono,
Email: o.email,
Estado: o.etapa || 'Sin etapa',
Origen: '',
Etapa: o.etapa || 'Sin etapa',
Origen: o.origen,
Desarrollo: o.desarrollo,
'Último cambio': o.ultimoCambio ? `${o.ultimoCambio.detalle} · ${o.ultimoCambio.usuario ?? ''}` : '',
Fuente: '',
Etapa: o.etapa,
}))
const ws = XLSX.utils.json_to_sheet(rows)
XLSX.utils.book_append_sheet(wb, ws, 'Oportunidades')