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:
2026-08-28 04:43:02 +00:00
parent 04de7d703f
commit 9e04b76616
16 changed files with 697 additions and 4 deletions

View File

@@ -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,