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

@@ -0,0 +1,42 @@
-- Tablas de actividades locales y asignación round-robin local.
-- Patrón manual: db execute + migrate resolve. NUNCA migrate dev.
-- CreateTable ActividadLocal
CREATE TABLE IF NOT EXISTS "ActividadLocal" (
"id" TEXT NOT NULL PRIMARY KEY,
"titulo" TEXT NOT NULL,
"descripcion" TEXT,
"usuarioId" TEXT NOT NULL,
"leadCapturaId" TEXT,
"fechaVencimiento" DATETIME,
"fechaCierre" DATETIME,
"estado" TEXT NOT NULL DEFAULT 'ABIERTA',
"tipo" TEXT NOT NULL DEFAULT 'SEGUIMIENTO',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS "ActividadLocal_usuarioId_idx" ON "ActividadLocal"("usuarioId");
CREATE INDEX IF NOT EXISTS "ActividadLocal_estado_idx" ON "ActividadLocal"("estado");
CREATE INDEX IF NOT EXISTS "ActividadLocal_fechaVencimiento_idx" ON "ActividadLocal"("fechaVencimiento");
-- CreateTable AsignacionLocal
CREATE TABLE IF NOT EXISTS "AsignacionLocal" (
"id" TEXT NOT NULL PRIMARY KEY,
"leadCapturaId" TEXT NOT NULL,
"usuarioId" TEXT NOT NULL,
"asignadoPor" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS "AsignacionLocal_leadCapturaId_key" ON "AsignacionLocal"("leadCapturaId");
CREATE INDEX IF NOT EXISTS "AsignacionLocal_usuarioId_idx" ON "AsignacionLocal"("usuarioId");
-- AlterTable LeadCaptura: add fechaLimite column
-- SQLite: CHECK if column exists first to avoid duplicate
-- The ALTER TABLE ADD COLUMN in SQLite is safe with IF NOT EXISTS semantics via PRAGMA
CREATE TABLE IF NOT EXISTS "__pragma_check" (name TEXT);
DROP TABLE IF EXISTS "__pragma_check";
-- ALTER TABLE is safe: SQLite ignores ADD COLUMN if column exists (error + ignore pattern)
-- We use a safer approach: add only if not present
-- SQLite doesn't support IF NOT EXISTS for ADD COLUMN, so we wrap in a safe pattern
ALTER TABLE "LeadCaptura" ADD COLUMN "fechaLimite" DATETIME;

View File

@@ -183,6 +183,8 @@ model LeadCaptura {
// Item de Monday una vez sincronizado (rows viejas: = leadId)
mondayItemId String?
// true = falta replicar a Monday (lo drena el sync worker)
// Fecha límite del apartado (para "Apartados por vencer" en Mi día)
fechaLimite DateTime?
pendienteSync Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt @default(now())
@@ -353,3 +355,35 @@ model AIMensaje {
@@index([conversacionId])
}
// ---------- Actividades locales (modo sin Monday) ----------
model ActividadLocal {
id String @id @default(uuid())
titulo String
descripcion String?
usuarioId String
leadCapturaId String?
fechaVencimiento DateTime?
fechaCierre DateTime?
estado String @default("ABIERTA") // ABIERTA | CERRADA
tipo String @default("SEGUIMIENTO") // SEGUIMIENTO | RECORDATORIO | REUNION
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([usuarioId])
@@index([estado])
@@index([fechaVencimiento])
}
// Asignaciones locales de leads (reemplaza round-robin de Monday)
model AsignacionLocal {
id String @id @default(uuid())
leadCapturaId String
usuarioId String
asignadoPor String? // round-robin | capturista | manual
createdAt DateTime @default(now())
@@unique([leadCapturaId])
@@index([usuarioId])
}