feat(modo-local): CRM 100% local sin dependencia de Monday.com

Nuevas tablas:
- ActividadLocal (historial de llamadas, reuniones, tareas)
- AsignacionLocal (round-robin local)
- fechaLimite en LeadCaptura

Servicios nuevos:
- actividad-local.service.ts (CRUD actividades)
- local-canal.ts (canal de envío para recordatorios)
- seed-modo-local.js (activar modo local)

Mi día local mejorado:
- Mis actividades hoy (filtros usuario/fecha/estado)
- Apartados por vencer (con fechaLimite real)
- Equipo (contadores por vendedor, solo coordinador/admin)
- unidadCodigo en oportunidades desde datosJson

Automatizaciones locales:
- Round-robin: asigna leads sin vendedor + crea actividad
- Inactividad: detecta oportunidades >10 días sin update
- Escalado: alerta oportunidades >48h sin contacto

Reportes: actividadesAbiertas + parcial:false en modo local
Cron wiring: crons locales cuando Monday caído

661 tests verdes.
This commit is contained in:
2026-09-07 04:00:15 +00:00
parent aadbc6f83f
commit 388e1da338
16 changed files with 1604 additions and 37 deletions

View File

@@ -296,6 +296,7 @@ export interface DatosLocalesReportes {
leads: LeadMonday[]
oportunidades: OportunidadMonday[]
unidades: Unidad[]
actividadesAbiertas?: number
}
/** Cargador de los datos locales para el fallback (Prisma en prod, fixture en tests) */
@@ -379,7 +380,10 @@ export async function cargarDatosLocalesPrisma(): Promise<DatosLocalesReportes>
loQuierePara: campoTexto(datos, 'loQuierePara'),
}
})
return { leads, oportunidades, unidades: unidadesRows.map(unidadLocalADto) }
// Contar actividades abiertas
const actividadesAbiertas = await prisma.actividadLocal.count({ where: { estado: 'ABIERTA' } })
return { leads, oportunidades, unidades: unidadesRows.map(unidadLocalADto), actividadesAbiertas }
}
export class ReportesService {
@@ -437,7 +441,13 @@ export class ReportesService {
}
}
const [locales, planes] = await Promise.all([this.cargarDatosLocales(), this.cargarPlanes()])
const resumen = construirResumen({ ...locales, planes, actividadesAbiertas: 0 }, new Date(ahoraMs))
return { ...resumen, parcial: true }
const resumen = construirResumen(
{ ...locales, planes, actividadesAbiertas: locales.actividadesAbiertas ?? 0 },
new Date(ahoraMs),
)
// En modo local, los datos son completos (no parciales): todas las métricas
// vienen de tablas locales. `parcial` solo se marca true cuando Monday cayó
// a media lectura (ya no aplica aquí).
return { ...resumen, parcial: false }
}
}