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

@@ -254,6 +254,12 @@ export class MondayService {
return unidades.find((u) => u.codigo === codigo) ?? null
}
/** Item ID de una unidad por código (para escrituras); null si no existe */
async itemIdDeUnidad(codigo: string): Promise<string | null> {
await this.getUnidades() // asegura cache fresco
return this.unidadesCache?.itemIds.get(codigo) ?? null
}
private mapearUnidad(item: ItemMonday, cols: Map<string, ColumnaMeta>): Unidad {
const porId = new Map(item.column_values.map((cv) => [cv.id, cv]))
const textoDe = (titulo: string): string => {
@@ -270,6 +276,9 @@ export class MondayService {
const colMeses = cols.get('Meses para firma')
const mesesTexto = colMeses ? (porId.get(colMeses.id)?.text ?? '') : ''
const mesesNum = Number(mesesTexto)
// La columna status "Etapa" (pipeline de 10 etapas) es opcional
const colEtapa = cols.get('Etapa')
const etapaTexto = colEtapa ? (porId.get(colEtapa.id)?.text ?? '') : ''
return {
codigo: item.name,
modelo: textoDe('Modelo'),
@@ -283,6 +292,7 @@ export class MondayService {
status: status || 'DISPONIBLE',
desarrollo: textoDe('Desarrollo'),
mesesParaFirma: mesesTexto !== '' && Number.isFinite(mesesNum) ? mesesNum : null,
etapa: etapaTexto !== '' ? etapaTexto : null,
}
}
@@ -757,10 +767,10 @@ export interface DatosMotivoNoCompra {
export interface DatosActividad {
nombre: string
tipoLabel: string // 'Llamada' | 'Reunion' | 'Tarea'
responsableId: number // user ID de Monday
responsableId?: number // user ID de Monday (se omite si no hay responsable claro)
fecha: string // 'YYYY-MM-DD'
estadoLabel: string // 'Abierto' | 'Listo'
leadItemId: string // item del tablero Leads relacionado
leadItemId?: string // item del tablero Leads relacionado (se omite si no aplica)
}
/** Singleton usado por los controllers (los tests instancian el suyo) */