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:
@@ -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')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user