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

@@ -92,7 +92,8 @@ export function crearDocumento(
doc.setFontSize(9)
doc.text('_______________________________', 105, y, { align: 'center' })
y += 5
doc.text(`Recibió — ${plan.clienteNombre}`, 105, y, { align: 'center' })
// Quien recibe el pago es FRASA (el cliente es quien paga)
doc.text('Recibió — FRASA DESARROLLOS S.A.P.I. DE C.V.', 105, y, { align: 'center' })
return { doc, nombreArchivo: `recibo_${plan.unidadCodigo}_${folio}.pdf` }
}

View File

@@ -1,20 +1,22 @@
import toast from "react-hot-toast"
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { useNavigate } from 'react-router-dom'
import { crearPlanAsync } from '../services/repository'
import { crearPlanAsync, listarProyectosAsync } from '../services/repository'
import { generarCalendario } from '../lib/cobranza'
import { fechaISOHoy, formatoFechaCorta, formatoMoneda } from '../lib/formatters'
const PROYECTOS = ['RESIDENCIAL MONTEBELLO', 'SENDA', 'DIAGONAL'] as const
// Proyecto libre (con sugerencias del datalist): la cartera real incluye
// desarrollos que un enum hardcodeado no cubría (ZAFIRO, UMBRAL, SOLE,
// DIAGONAL 40, SOLANA I/II...), así que ya no se limita a una lista fija.
const esquema = z.object({
clienteNombre: z.string().trim().min(3, 'Nombre del cliente requerido (mín. 3 caracteres)'),
clienteTelefono: z.string().trim().optional(),
unidadCodigo: z.string().trim().min(1, 'Código de unidad requerido'),
proyecto: z.enum(PROYECTOS),
proyecto: z.string().trim().min(1, 'Proyecto requerido'),
ventaRef: z.string().trim(),
fechaInicio: z.string().min(1, 'Fecha de inicio requerida'),
engancheTotal: z
@@ -46,6 +48,11 @@ export default function NuevoPlanPage() {
const navigate = useNavigate()
const [guardando, setGuardando] = useState(false)
const [errorGuardado, setErrorGuardado] = useState<string | null>(null)
// Proyectos existentes en la cartera: alimentan el datalist de sugerencias
const [proyectosSugeridos, setProyectosSugeridos] = useState<string[]>([])
useEffect(() => {
void listarProyectosAsync().then(setProyectosSugeridos)
}, [])
const {
register,
handleSubmit,
@@ -56,7 +63,7 @@ export default function NuevoPlanPage() {
defaultValues: {
clienteNombre: '',
unidadCodigo: '',
proyecto: 'SENDA',
proyecto: '',
ventaRef: '',
fechaInicio: fechaISOHoy(),
numPagos: 6,
@@ -122,13 +129,19 @@ export default function NuevoPlanPage() {
</div>
<div>
<label htmlFor="proyecto" className={claseLabel}>Proyecto *</label>
<select id="proyecto" className={claseInput} {...register('proyecto')}>
{PROYECTOS.map((p) => (
<option key={p} value={p}>
{p}
</option>
<input
id="proyecto"
list="proyectos-cobranza"
className={claseInput}
placeholder="Ej. SENDA, ZAFIRO, DIAGONAL 40…"
{...register('proyecto')}
/>
<datalist id="proyectos-cobranza">
{proyectosSugeridos.map((p) => (
<option key={p} value={p} />
))}
</select>
</datalist>
{errors.proyecto && <p className={claseError}>{errors.proyecto.message}</p>}
</div>
<div>
<label htmlFor="ventaRef" className={claseLabel}>Referencia de venta</label>

View File

@@ -365,3 +365,13 @@ export async function actualizarPlanAsync(
export function sinPlanesAsignados(resultado: ResultadoPlanes, mios: boolean): boolean {
return mios && resultado.origen === 'api' && resultado.planes.length === 0
}
/** GET /planes — proyectos distintos de la cartera (datalist del alta de plan) */
export async function listarProyectosAsync(): Promise<string[]> {
try {
const planes = await request<PlanApi[]>('/planes')
return [...new Set(planes.map((p) => p.proyecto).filter((p) => typeof p === 'string' && p.trim() !== ''))].sort()
} catch {
return []
}
}