feat(leads): doble via de captura — lead rapido a Leads, afluencia a Oportunidades
- POST /api/leads: captura ligera al tablero Leads (sin oportunidad), self-assign a Vendedor o round-robin; tambien cubre la captura manual desde la UI de Monday (webhook create_item re-registrado). - POST /api/afluencias (nuevo): la forma completa sigue cayendo en Oportunidades con todo el perfil (etapa Afluencia, columna Asesor). - Round-robin dual (ambos tableros), Mi dia mezcla leads+oportunidades con insignia de origen, acciones con parametro fuente, reportes: leads desde Leads, perfilamiento desde Oportunidades, funnel Leads->Afluencia->...->Ganado.
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import { statusPago, statusPlan, type PlanPago } from './cobranza.service.js'
|
||||
import type { MondayService, OportunidadMonday, Unidad } from './monday.service.js'
|
||||
import type { LeadMonday, MondayService, OportunidadMonday, Unidad } from './monday.service.js'
|
||||
import { ETAPAS_PIPELINE } from './etapas.js'
|
||||
|
||||
/**
|
||||
* Agregador de reportes para la app de tableros (/dash/).
|
||||
* En UNA respuesta junta: afluencia/perfil (Monday, tablero Oportunidades — la
|
||||
* afluencia ES la oportunidad en etapa 'Afluencia'), oportunidades (Monday),
|
||||
* unidades (Monday), cobranza (Prisma local) y actividades abiertas (Monday).
|
||||
* En UNA respuesta junta: leads (Monday, tablero Leads — captura ligera),
|
||||
* afluencia/perfilamiento (Monday, tablero Oportunidades — el Alta Afluencia
|
||||
* vive ahí), oportunidades (Monday), unidades (Monday), cobranza (Prisma
|
||||
* local) y actividades abiertas (Monday).
|
||||
* Cache en memoria de 60 s para no martillar la API de Monday.
|
||||
*/
|
||||
|
||||
@@ -76,8 +77,10 @@ export interface ResumenReportes {
|
||||
|
||||
/** Datos crudos que necesita el agregador (inyectables en tests) */
|
||||
export interface DatosResumen {
|
||||
// La sección `leads` (afluencia + perfilamiento) y la sección `oportunidades`
|
||||
// se alimentan de la MISMA fuente: el tablero Oportunidades
|
||||
// La sección `leads` (total/nuevos/porEstado/porOrigen) lee el tablero Leads
|
||||
// (captura ligera); el `perfil` de la afluencia lee Oportunidades (Alta
|
||||
// Afluencia), que es también la fuente de la sección `oportunidades`
|
||||
leads: LeadMonday[]
|
||||
oportunidades: OportunidadMonday[]
|
||||
unidades: Unidad[]
|
||||
planes: PlanPago[] // dominio de cobranza (con pagos)
|
||||
@@ -164,8 +167,8 @@ export function construirResumen(datos: DatosResumen, ahora: Date): ResumenRepor
|
||||
const hoy = inicioDelDia(ahora)
|
||||
const hace7dias = new Date(hoy.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||
const creadosEn = (desde: Date): number =>
|
||||
datos.oportunidades.filter((o) => {
|
||||
const t = Date.parse(o.createdAt)
|
||||
datos.leads.filter((l) => {
|
||||
const t = Date.parse(l.createdAt)
|
||||
return !Number.isNaN(t) && t >= desde.getTime()
|
||||
}).length
|
||||
|
||||
@@ -221,14 +224,14 @@ export function construirResumen(datos: DatosResumen, ahora: Date): ResumenRepor
|
||||
|
||||
return {
|
||||
generadoEn: ahora.toISOString(),
|
||||
// Sección "leads" = afluencia sobre el tablero Oportunidades (modelo
|
||||
// nuevo: la afluencia ES la oportunidad en etapa 'Afluencia')
|
||||
// Sección "leads" = tablero Leads (captura ligera); el perfilamiento de la
|
||||
// afluencia se lee de Oportunidades (el Alta Afluencia vive ahí)
|
||||
leads: {
|
||||
total: datos.oportunidades.length,
|
||||
total: datos.leads.length,
|
||||
nuevosHoy: creadosEn(hoy),
|
||||
nuevosSemana: creadosEn(hace7dias),
|
||||
porEstado: contarPor(datos.oportunidades.map((o) => o.etapa)),
|
||||
porOrigen: contarPor(datos.oportunidades.map((o) => o.origen)),
|
||||
porEstado: contarPor(datos.leads.map((l) => l.estado)),
|
||||
porOrigen: contarPor(datos.leads.map((l) => l.origen)),
|
||||
perfil: {
|
||||
porDesarrollo: contarPorNoVacios(datos.oportunidades.map((o) => o.desarrollo ?? '')),
|
||||
porModelo: contarPorNoVacios(datos.oportunidades.map((o) => o.modeloInteres ?? '')),
|
||||
@@ -277,7 +280,7 @@ export function construirResumen(datos: DatosResumen, ahora: Date): ResumenRepor
|
||||
|
||||
// ---------- Servicio con cache ----------
|
||||
|
||||
type MondayReportes = Pick<MondayService, 'getOportunidades' | 'getUnidades' | 'getActividadesAbiertas'>
|
||||
type MondayReportes = Pick<MondayService, 'getLeads' | 'getOportunidades' | 'getUnidades' | 'getActividadesAbiertas'>
|
||||
|
||||
/** Carga los planes de cobranza en forma de dominio (Prisma en prod, fixture en tests) */
|
||||
export type CargadorPlanes = () => Promise<PlanPago[]>
|
||||
@@ -313,13 +316,14 @@ export class ReportesService {
|
||||
if (this.cache && this.cache.expira > ahoraMs) return this.cache.resumen
|
||||
|
||||
// Las fuentes de Monday se piden en paralelo; Prisma va por su lado
|
||||
const [oportunidades, unidades, actividadesAbiertas, planes] = await Promise.all([
|
||||
const [leads, oportunidades, unidades, actividadesAbiertas, planes] = await Promise.all([
|
||||
this.monday.getLeads(),
|
||||
this.monday.getOportunidades(),
|
||||
this.monday.getUnidades(),
|
||||
this.monday.getActividadesAbiertas(),
|
||||
this.cargarPlanes(),
|
||||
])
|
||||
const resumen = construirResumen({ oportunidades, unidades, planes, actividadesAbiertas }, new Date(ahoraMs))
|
||||
const resumen = construirResumen({ leads, oportunidades, unidades, planes, actividadesAbiertas }, new Date(ahoraMs))
|
||||
this.cache = { resumen, expira: ahoraMs + this.cacheMs }
|
||||
return resumen
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user