feat(ux): 17 mejoras de experiencia de usuario

Backend (10 endpoints nuevos):
- Recordatorios personales (CRUD + tabla RecordatorioVendedor)
- Lead scoring 0-100 (etapa+recencia+datos)
- Dashboard mora (pagos vencidos, monto, días atraso)
- Timeline de pagos (pagado/vence/futuro)
- Perfil completo contacto (pagos+cotizaciones+docs+score)
- Exportar contactos a CSV

Frontend Mi día:
- Resumen matutino (saludo+KPIs)
- Historial visual lead (timeline)
- Score badge 0-100 (verde/amarillo/rojo)
- Vista kanban (drag & drop)

Frontend Cobranza:
- Dashboard mora (tabla+WhatsApp)
- Timeline pagos visual (nodos coloreados)

Frontend Alta Clientes:
- Checklist documentos visual
- Foto cliente (cámara)

Frontend Landing:
- Perfil contacto completo
- Exportar contactos CSV

Frontend Mi día (extras):
- Comparativa mensual
- KPIs dashboard (leads hoy, ventas, conversión)

806 tests, 5 apps construidas.
This commit is contained in:
2026-09-08 07:42:54 +00:00
parent 5063ffe281
commit db18685872
34 changed files with 2950 additions and 33 deletions

View File

@@ -1,8 +1,10 @@
import { useEffect, useRef } from 'react'
import { useFormContext } from 'react-hook-form'
import { CampoTexto, CampoSelect, CampoTelefono } from '../components/Campos'
import SectionCard from '../components/SectionCard'
import { compararConDatos } from '../lib/curp'
import { ESTADOS_CIVILES, REGIMENES_BIENES, TIPOS_VIVIENDA, TIPOS_COMPRA } from '../data/constantes'
import { autocompletarPorCP } from '../data/cps'
interface Props {
prefix: string
@@ -11,7 +13,7 @@ interface Props {
/** Campos de datos generales; reutilizable con prefijo (cliente y co-acreditado) */
export default function DatosGeneralesCampos({ prefix, compacto = false }: Props) {
const { watch } = useFormContext()
const { watch, setValue } = useFormContext()
const p = (campo: string) => `${prefix}${campo}`
const estadoCivil = watch(p('estadoCivil')) as string
const curp = (watch(p('curp')) as string) ?? ''
@@ -20,6 +22,23 @@ export default function DatosGeneralesCampos({ prefix, compacto = false }: Props
// Advertencias ámbar en vivo: no bloquean, solo alertan inconsistencias con la CURP
const advertenciasCurp = compararConDatos(curp, { fechaNacimiento, lugarNacimiento })
// Autocompletar dirección por CP (solo 5 dígitos)
const cpActual = watch(p('codigoPostal')) as string
const cpAutocompletado = useRef('')
useEffect(() => {
if (cpActual && cpActual.length === 5 && cpActual !== cpAutocompletado.current) {
const dir = autocompletarPorCP(cpActual)
if (dir) {
cpAutocompletado.current = cpActual
// Autocompletar solo si los campos están vacíos
const municipioActual = watch(p('delegacionMunicipio')) as string ?? ''
if (!municipioActual) {
setValue(p('delegacionMunicipio'), dir.municipio)
}
}
}
}, [cpActual])
return (
<SectionCard titulo="Datos generales">
{!compacto && <CampoTexto nombre={p('campaniaVenta')} etiqueta="Campaña de la venta" />}