feat(unidades): pipeline de 10 etapas por unidad + sync desde Reporte_EstatusVenta
- Columna Etapa en Monday Unidades (10 labels oficiales), 155 unidades pobladas - sync-etapas.ts idempotente (--archivo/--dry-run/--semilla) con traspasos Vendido→Escrituración y Escriturado→SAC como actividades (sin retroactivas) - API: etapa en unidades + porEtapa en /api/reportes/resumen - Dashboard: gráfica pipeline por etapa + KPI en notaría; cotizador muestra etapa - 28 cambios aplicados desde Reporte_EstatusVenta (8 avances, 20 nuevas); contradicciones retenidas para revisión humana (documentadas) - Tests: api 168, dashboards 9, cotizador 71
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { statusPago, statusPlan, type PlanPago } from './cobranza.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/).
|
||||
@@ -44,6 +45,8 @@ export interface ResumenReportes {
|
||||
total: number
|
||||
disponibles: number
|
||||
porDesarrollo: { desarrollo: string; total: number; porStatus: Record<string, number> }[]
|
||||
/** Conteo por etapa del pipeline de venta, en el orden oficial de las 10 etapas */
|
||||
porEtapa: { etapa: string; total: number }[]
|
||||
}
|
||||
cobranza: {
|
||||
planes: { activos: number; liquidados: number; cancelados: number }
|
||||
@@ -105,6 +108,7 @@ export function construirResumen(datos: DatosResumen, ahora: Date): ResumenRepor
|
||||
).length
|
||||
|
||||
const desarrollos = new Map<string, { total: number; porStatus: Record<string, number> }>()
|
||||
const conteoEtapas = new Map<string, number>()
|
||||
for (const u of datos.unidades) {
|
||||
const clave = u.desarrollo.trim() === '' ? '(sin desarrollo)' : u.desarrollo.trim()
|
||||
const entrada = desarrollos.get(clave) ?? { total: 0, porStatus: {} }
|
||||
@@ -112,7 +116,16 @@ export function construirResumen(datos: DatosResumen, ahora: Date): ResumenRepor
|
||||
const status = u.status.trim() === '' ? '(sin status)' : u.status.trim().toUpperCase()
|
||||
entrada.porStatus[status] = (entrada.porStatus[status] ?? 0) + 1
|
||||
desarrollos.set(clave, entrada)
|
||||
const etapa = u.etapa?.trim()
|
||||
if (etapa) conteoEtapas.set(etapa, (conteoEtapas.get(etapa) ?? 0) + 1)
|
||||
}
|
||||
// Etapas oficiales primero (en orden del pipeline); cualquier label desconocido va al final
|
||||
const porEtapaUnidades = [
|
||||
...ETAPAS_PIPELINE.map((etapa) => ({ etapa, total: conteoEtapas.get(etapa) ?? 0 })),
|
||||
...[...conteoEtapas.keys()]
|
||||
.filter((e) => !(ETAPAS_PIPELINE as readonly string[]).includes(e))
|
||||
.map((etapa) => ({ etapa, total: conteoEtapas.get(etapa) ?? 0 })),
|
||||
]
|
||||
|
||||
const planesPorStatus = { activos: 0, liquidados: 0, cancelados: 0 }
|
||||
let planesAlCorriente = 0
|
||||
@@ -154,6 +167,7 @@ export function construirResumen(datos: DatosResumen, ahora: Date): ResumenRepor
|
||||
porDesarrollo: [...desarrollos.entries()]
|
||||
.map(([desarrollo, v]) => ({ desarrollo, total: v.total, porStatus: v.porStatus }))
|
||||
.sort((a, b) => a.desarrollo.localeCompare(b.desarrollo, 'es')),
|
||||
porEtapa: porEtapaUnidades,
|
||||
},
|
||||
cobranza: {
|
||||
planes: planesPorStatus,
|
||||
|
||||
Reference in New Issue
Block a user