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:
89
api-gateway/src/services/mora.service.ts
Normal file
89
api-gateway/src/services/mora.service.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { prisma } from './prisma.js'
|
||||
|
||||
/**
|
||||
* Dashboard de mora: clientes con pagos vencidos (fechaVencimiento < hoy
|
||||
* y montoPagado < monto). Devuelve resumen + detalle por cliente.
|
||||
*/
|
||||
|
||||
export interface ClienteMora {
|
||||
clienteNombre: string
|
||||
unidadCodigo: string
|
||||
proyecto: string
|
||||
pagoVencido: number
|
||||
monto: number
|
||||
diasAtraso: number
|
||||
telefono: string | null
|
||||
planId: string
|
||||
}
|
||||
|
||||
export interface ResumenMora {
|
||||
clientesConMora: number
|
||||
montoTotalVencido: number
|
||||
diasPromedioAtraso: number
|
||||
clientes: ClienteMora[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula días naturales de diferencia entre hoy y la fecha de vencimiento.
|
||||
*/
|
||||
function diasDesde(fechaVencimiento: Date, hoy: Date): number {
|
||||
const hv = new Date(hoy.getFullYear(), hoy.getMonth(), hoy.getDate())
|
||||
const fv = new Date(
|
||||
fechaVencimiento.getFullYear(),
|
||||
fechaVencimiento.getMonth(),
|
||||
fechaVencimiento.getDate(),
|
||||
)
|
||||
return Math.max(0, Math.round((hv.getTime() - fv.getTime()) / 86_400_000))
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumen de mora: todos los pagos vencidos de planes activos.
|
||||
*/
|
||||
export async function resumenMora(): Promise<ResumenMora> {
|
||||
const hoy = new Date()
|
||||
const inicioHoy = new Date(hoy.getFullYear(), hoy.getMonth(), hoy.getDate())
|
||||
|
||||
const planes = await prisma.planPago.findMany({
|
||||
where: { status: { not: 'CANCELADO' } },
|
||||
include: { pagos: true },
|
||||
})
|
||||
|
||||
const clientes: ClienteMora[] = []
|
||||
|
||||
for (const plan of planes) {
|
||||
for (const pago of plan.pagos) {
|
||||
const restante = pago.monto - pago.montoPagado
|
||||
if (restante <= 0) continue // ya pagado
|
||||
const fv = pago.fechaVencimiento
|
||||
if (fv >= inicioHoy) continue // aún no vence
|
||||
|
||||
const dias = diasDesde(fv, hoy)
|
||||
clientes.push({
|
||||
clienteNombre: plan.clienteNombre,
|
||||
unidadCodigo: plan.unidadCodigo,
|
||||
proyecto: plan.proyecto,
|
||||
pagoVencido: pago.numPago,
|
||||
monto: Math.round(restante * 100) / 100,
|
||||
diasAtraso: dias,
|
||||
telefono: plan.clienteTelefono,
|
||||
planId: plan.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Ordenar por días de atraso (mayor primero)
|
||||
clientes.sort((a, b) => b.diasAtraso - a.diasAtraso)
|
||||
|
||||
const montoTotalVencido = Math.round(clientes.reduce((acc, c) => acc + c.monto, 0) * 100) / 100
|
||||
const diasPromedioAtraso =
|
||||
clientes.length > 0
|
||||
? Math.round(clientes.reduce((acc, c) => acc + c.diasAtraso, 0) / clientes.length)
|
||||
: 0
|
||||
|
||||
return {
|
||||
clientesConMora: clientes.length,
|
||||
montoTotalVencido,
|
||||
diasPromedioAtraso,
|
||||
clientes,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user