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

@@ -209,3 +209,79 @@ export async function detalleContactoLocal(contactoId: string): Promise<DetalleC
return { contacto, propiedad, relaciones, historial }
}
// ── Exportar contactos a CSV ─────────────────────────────────────────
/**
* Genera CSV de todos los contactos (Cliente + LeadCaptura).
* Columnas: Nombre, Teléfono, Email, Desarrollo, Estatus, Última actividad.
*/
export async function exportarCSV(filtros: { desarrollo?: string; estatus?: string } = {}): Promise<string> {
const clientes = await prisma.cliente.findMany({ orderBy: { createdAt: 'desc' } })
const capturas = await prisma.leadCaptura.findMany({
where: {
board: { in: ['OPORTUNIDADES', 'APARTADO', 'LEADS'] },
},
orderBy: { updatedAt: 'desc' },
})
interface FilaCSV {
Nombre: string
Teléfono: string
Email: string
Desarrollo: string
Estatus: string
'Última actividad': string
}
const filas: FilaCSV[] = []
const vistos = new Set<string>()
for (const c of clientes) {
const clave = (c.celular ?? '').replace(/\D/g, '').slice(-10)
if (clave !== '' && vistos.has(clave)) continue
if (clave !== '') vistos.add(clave)
filas.push({
Nombre: c.nombreCompleto,
Teléfono: c.celular ?? '',
Email: c.email ?? '',
Desarrollo: c.proyecto,
Estatus: 'Cliente',
'Última actividad': c.createdAt.toISOString().slice(0, 10),
})
}
for (const cap of capturas) {
const clave = (cap.telefono ?? '').replace(/\D/g, '').slice(-10)
if (clave !== '' && vistos.has(clave)) continue
if (clave !== '') vistos.add(clave)
filas.push({
Nombre: cap.nombreLead,
Teléfono: cap.telefono ?? '',
Email: cap.email ?? '',
Desarrollo: cap.desarrollo ?? '',
Estatus: cap.etapa,
'Última actividad': cap.updatedAt.toISOString().slice(0, 10),
})
}
// Aplicar filtros
let filtradas = filas
if (filtros.desarrollo) {
const d = filtros.desarrollo.toUpperCase()
filtradas = filtradas.filter((f) => f.Desarrollo.toUpperCase().includes(d))
}
if (filtros.estatus) {
const e = filtros.estatus.toUpperCase()
filtradas = filtradas.filter((f) => f.Estatus.toUpperCase().includes(e))
}
// Generar CSV
const encabezado = 'Nombre,Teléfono,Email,Desarrollo,Estatus,Última actividad'
const escape = (s: string) => (s.includes(',') || s.includes('"') ? `"${s.replace(/"/g, '""')}"` : s)
const lineas = filtradas.map((f) =>
[f.Nombre, f.Teléfono, f.Email, f.Desarrollo, f.Estatus, f['Última actividad']].map(escape).join(',')
)
return [encabezado, ...lineas].join('\n')
}