fix(auditoria general): 25 errores encontrados en la revision modulo por modulo
SEGURIDAD (agente IA): - Conversaciones: GET /ai/conversations/:id y el reanudar chat verifican propiedad (antes cualquier usuario leia/continuaba la conversacion de otro). - /ai/documents pasa a solo-admin (antes cualquien podia inyectar o borrar documentos de la base de conocimiento). DATOS / CORRUPCION: - Cotizador: elegir un cliente del maestro en el buscador ya no crea un lead fantasma "Registro previo (CRM anterior)" en el embudo. - Afluencia con oportunidadId inexistente: 404 en vez de crear sombra fantasma. - Cobranza: los 103 planes importados quedaron con vendedor asignado (7 con su asesor real segun el Excel, el resto al admin) y el motor de recordatorios cae al admin cuando un plan no tiene vendedor: antes los 411 recordatorios se registraban 'sin-destinatario' y no los veia nadie. - Backups: script reescrito para el host real (sqlite .backup + uploads + env en /root/backups) y cron instalado (antes: script que asumia docker y sin cron programado = cero respaldos). REPORTES / METRICAS: - Mi tablero: el embudo ya excluye cancelaciones reales (Cancelo/Cancelacion Apartado) y cuenta como vendidos Firma Cliente/Escriturado/Titulado (antes 41 "apartados" donde habia 13 activos y vendidos buscaba una etapa que no existe); tasa de descarte ya compara el mismo periodo. - Scoring: etapa real 'Agendo Cita' (antes 'AGENDO' nunca aplicaba) y los descartados/perdidos ya no reciben score. - ver_cobranza (IA): ignora planes cancelados y no cuenta como vencido el pago que vence hoy. UI / FLUJOS: - mi-dia: typo CANCO -> CANCELO (los cancelados reaparecen en Escrituracion); confirmar cita ya no retrocede la etapa de Cotizador/Aparto (boton y kanban). - alta-clientes: la firma ya no se borra al navegar fuera del paso de firmas; toast de exito solo cuando el API confirma; prefill de apartado trae direccion, estado civil y fecha de nacimiento desde el alta. - Auditoria: el filtro por usuario funciona (el front enviaba 'usuario' y el API esperaba 'usuarioId') y 'hasta' incluye todo el dia. - Contactos: paginacion aplicada despues de fusionar fuentes (antes saltaban o se repetian contactos entre paginas y 'Siguiente' se deshabilitaba antes de tiempo). - Buscador global: leads -> /hoy/, contactos -> /; el cotizador entiende ?cotizacion=<id> para retomar desde la liga. - Export mi-dia: columnas Desarrollo/Origen rellenas y sin duplicados. - Recibo de cobranza: "Recibio" es FRASA (el cliente paga, no recibe). - Alta de plan de cobranza: proyecto libre con sugerencias del catalogo real (antes limitado a 3 proyectos que no cubrian la cartera). Verificado en vivo: autorizacion IA (3x 403), filtro de auditoria, 404 de afluencia. Suites: 717 API + 124 alta-clientes + 115 cotizador + 92 cobranza + 89 landing + 32 mi-dia en verde. Backup de hoy generado en /root/backups.
This commit is contained in:
@@ -2,6 +2,7 @@ import crypto from 'node:crypto'
|
||||
import { prisma } from './prisma.js'
|
||||
import { config } from '../config/index.js'
|
||||
import { AppError, NotFoundError } from '../utils/errors.js'
|
||||
import { esAdminOManager } from '../utils/roles.js'
|
||||
import { getUnidadesLocal } from './unidades-local.service.js'
|
||||
import {
|
||||
APARTADO_DEFAULT,
|
||||
@@ -1020,10 +1021,12 @@ export function crearEjecutoresHerramientas(
|
||||
})
|
||||
if (planes.length === 0) return { error: `No hay planes de cobranza para "${cliente}"` }
|
||||
const hoy = new Date()
|
||||
// Medianoche de hoy: lo que vence HOY aún no está vencido
|
||||
const inicioHoy = new Date(hoy.getFullYear(), hoy.getMonth(), hoy.getDate())
|
||||
return planes.map((p) => {
|
||||
const pagado = p.pagos.reduce((a, x) => a + x.montoPagado, 0)
|
||||
const saldo = p.pagos.reduce((a, x) => a + (x.monto - x.montoPagado), 0)
|
||||
const vencidos = p.pagos.filter((x) => x.fechaVencimiento < hoy && x.montoPagado < x.monto)
|
||||
const vencidos = p.pagos.filter((x) => x.fechaVencimiento < inicioHoy && x.montoPagado < x.monto)
|
||||
const proximo = p.pagos.filter((x) => x.montoPagado < x.monto && x.fechaVencimiento >= hoy)
|
||||
.sort((a, b) => a.fechaVencimiento.getTime() - b.fechaVencimiento.getTime())[0]
|
||||
const abonos = p.pagos
|
||||
@@ -1054,7 +1057,8 @@ export function crearEjecutoresHerramientas(
|
||||
prisma.planPago.count({ where: { status: 'ACTIVO', ...alcance } }),
|
||||
prisma.planPago.count({ where: { status: 'CANCELADO', ...alcance } }),
|
||||
prisma.pago.findMany({
|
||||
where: { fechaVencimiento: { lt: new Date() }, plan: alcance },
|
||||
// Sólo planes activos: el adeudo de planes cancelados no cuenta
|
||||
where: { fechaVencimiento: { lt: new Date() }, plan: { ...alcance, status: 'ACTIVO' } },
|
||||
select: {
|
||||
monto: true,
|
||||
montoPagado: true,
|
||||
@@ -1505,6 +1509,18 @@ export async function chat(
|
||||
? await prisma.aIConversacion.findUnique({ where: { id: sessionId } })
|
||||
: null
|
||||
if (sessionId && !conversacion) throw new NotFoundError('Conversación no encontrada')
|
||||
// Propiedad: sólo el dueño (o un rol de supervisión) puede reanudar una
|
||||
// conversación existente. Sin esto, cualquiera con el uuid continuaba el
|
||||
// chat ajeno con todo su historial.
|
||||
if (
|
||||
conversacion &&
|
||||
opciones.usuarioId &&
|
||||
conversacion.usuarioId &&
|
||||
conversacion.usuarioId !== opciones.usuarioId &&
|
||||
!esAdminOManager(opciones.contexto?.rol)
|
||||
) {
|
||||
throw new AppError(403, 'No puedes continuar la conversación de otro usuario')
|
||||
}
|
||||
conversacion ??= await prisma.aIConversacion.create({
|
||||
data: { usuarioId: opciones.usuarioId ?? null },
|
||||
})
|
||||
|
||||
@@ -94,7 +94,8 @@ describe('buscar', () => {
|
||||
prismaMock.usuario.findMany.mockResolvedValue([])
|
||||
await buscar({ desde, hasta })
|
||||
expect(prismaMock.auditoriaLog.findMany).toHaveBeenCalledWith({
|
||||
where: { createdAt: { gte: desde, lte: hasta } },
|
||||
// "hasta" incluye todo el día: se excluye con lt al día siguiente
|
||||
where: { createdAt: { gte: desde, lt: new Date(hasta.getTime() + 24 * 60 * 60 * 1000) } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 100,
|
||||
})
|
||||
|
||||
@@ -43,7 +43,9 @@ export async function buscar(filtros: FiltrosAuditoria, limite = 100) {
|
||||
if (filtros.desde !== undefined || filtros.hasta !== undefined) {
|
||||
where['createdAt'] = {
|
||||
...(filtros.desde !== undefined ? { gte: filtros.desde } : {}),
|
||||
...(filtros.hasta !== undefined ? { lte: filtros.hasta } : {}),
|
||||
// "hasta" es inclusive de todo el día: se excluye con lt al día siguiente
|
||||
// (antes con lte a las 00:00 del día se perdía casi todo el día elegido)
|
||||
...(filtros.hasta !== undefined ? { lt: new Date(filtros.hasta.getTime() + 24 * 60 * 60 * 1000) } : {}),
|
||||
}
|
||||
}
|
||||
const logs = await prisma.auditoriaLog.findMany({
|
||||
|
||||
@@ -155,8 +155,8 @@ describe('buscar (GET /api/buscar)', () => {
|
||||
])
|
||||
|
||||
const r = await buscar('X')
|
||||
expect(r.find((i) => i.tipo === 'lead')?.url).toBe('/app/#leads')
|
||||
expect(r.find((i) => i.tipo === 'contacto')?.url).toBe('/app/#clientes')
|
||||
expect(r.find((i) => i.tipo === 'lead')?.url).toBe('/hoy/')
|
||||
expect(r.find((i) => i.tipo === 'contacto')?.url).toBe('/')
|
||||
expect(r.find((i) => i.tipo === 'oportunidad')?.url).toBe('/quote/?cotizacion=ct1')
|
||||
expect(r.find((i) => i.tipo === 'unidad')?.url).toBe('/quote/?unidad=X')
|
||||
})
|
||||
|
||||
@@ -88,14 +88,14 @@ export async function buscar(
|
||||
id: l.leadId,
|
||||
nombre: l.nombreLead,
|
||||
detalle: [l.telefono, l.email].filter(Boolean).join(' · ') || l.origen || l.board,
|
||||
url: `/app/#leads`,
|
||||
url: `/hoy/`,
|
||||
})),
|
||||
...clientes.map((c) => ({
|
||||
tipo: 'contacto' as const,
|
||||
id: c.id,
|
||||
nombre: c.nombreCompleto,
|
||||
detalle: [c.email, c.celular, c.proyecto].filter(Boolean).join(' · ') || '',
|
||||
url: `/app/#clientes`,
|
||||
url: `/`,
|
||||
})),
|
||||
...cotizaciones.map((ct) => ({
|
||||
tipo: 'oportunidad' as const,
|
||||
|
||||
@@ -212,7 +212,8 @@ describe('listarContactosLocal', () => {
|
||||
})
|
||||
|
||||
it('pagina de 100 en 100 con hayMas (la página 2 trae el siguiente bloque)', async () => {
|
||||
// 101 clientes: la página 1 llena 100 y detecta que hay más
|
||||
// 101 clientes: la paginación se aplica DESPUÉS de fusionar fuentes, así
|
||||
// que el servicio pide el universo completo y corta en memoria
|
||||
prismaMock.cliente.findMany.mockResolvedValue(
|
||||
Array.from({ length: 101 }, (_, i) =>
|
||||
clienteFake({ id: `c${i + 1}`, celular: `4770000${String(i).padStart(3, '0')}`, createdAt: new Date(2026, 8, 1, 0, 0, 0, i * 1000) }),
|
||||
@@ -225,13 +226,12 @@ describe('listarContactosLocal', () => {
|
||||
expect(pagina1.contactos).toHaveLength(100)
|
||||
expect(pagina1.hayMas).toBe(true)
|
||||
|
||||
// La página 2 recibe el siguiente bloque (skip=100, take=101)
|
||||
prismaMock.cliente.findMany.mockResolvedValue([
|
||||
clienteFake({ id: 'c101', celular: '4770000100' }),
|
||||
])
|
||||
// La página 2 trae la fila más vieja (orden desc por fecha: la 101 va
|
||||
// primero en la página 1)
|
||||
const pagina2 = await listarContactosLocal(undefined, 2)
|
||||
expect(pagina2.pagina).toBe(2)
|
||||
expect(pagina2.contactos).toHaveLength(1)
|
||||
expect(pagina2.contactos[0]?.id).toBe('c1')
|
||||
expect(pagina2.hayMas).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -92,12 +92,12 @@ export async function listarContactosLocal(usuarioId?: string, pagina = 1): Prom
|
||||
const porPagina = CONTACTOS_POR_PAGINA
|
||||
const paginaValida = Number.isFinite(pagina) && pagina >= 1 ? Math.floor(pagina) : 1
|
||||
const skip = (paginaValida - 1) * porPagina
|
||||
// +1 por fuente para detectar si queda más (hayMas) sin una consulta extra
|
||||
// La paginación se aplica DESPUÉS de fusionar las dos fuentes: antes se hacía
|
||||
// skip/take por fuente y el dedupe posterior perdía contactos entre páginas
|
||||
// y daba hayMas=false antes de tiempo.
|
||||
const clientes = await prisma.cliente.findMany({
|
||||
where: usuarioId ? { capturadoPorId: usuarioId } : {},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip,
|
||||
take: porPagina + 1,
|
||||
})
|
||||
const capturas = await prisma.leadCaptura.findMany({
|
||||
where: {
|
||||
@@ -105,8 +105,6 @@ export async function listarContactosLocal(usuarioId?: string, pagina = 1): Prom
|
||||
...(usuarioId ? { usuarioId } : {}),
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
skip,
|
||||
take: porPagina + 1,
|
||||
})
|
||||
|
||||
const lista: Candidato[] = []
|
||||
@@ -207,12 +205,11 @@ export async function listarContactosLocal(usuarioId?: string, pagina = 1): Prom
|
||||
})
|
||||
}
|
||||
const ordenada = lista.map((c) => c.contacto).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
||||
const hayMas = ordenada.length > porPagina
|
||||
return {
|
||||
contactos: ordenada.slice(0, porPagina),
|
||||
contactos: ordenada.slice(skip, skip + porPagina),
|
||||
pagina: paginaValida,
|
||||
porPagina,
|
||||
hayMas,
|
||||
hayMas: ordenada.length > skip + porPagina,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,27 +124,11 @@ export class CrmLocalService {
|
||||
|
||||
if (oportunidadId !== undefined) {
|
||||
const existente = await prisma.leadCaptura.findUnique({ where: { leadId: oportunidadId } })
|
||||
// Sin fila local (id heredado del CRM anterior): se crea la sombra con
|
||||
// ese id para que la captura no se pierda en silencio.
|
||||
// Sin fila local: id inválido o liga guardada de una limpieza anterior.
|
||||
// Antes se creaba una oportunidad "sombra" fantasma con ese id; ahora se
|
||||
// rechaza para no sembrar registros huérfanos en Oportunidades.
|
||||
if (!existente) {
|
||||
await prisma.leadCaptura.create({
|
||||
data: {
|
||||
leadId: oportunidadId,
|
||||
oportunidadId,
|
||||
usuarioId: usuarioId ?? 'desconocido',
|
||||
nombreLead: datos.nombre,
|
||||
telefono: datos.telefono ?? null,
|
||||
email: datos.email ?? null,
|
||||
origen: datos.origen ?? null,
|
||||
desarrollo: datos.desarrollo ?? null,
|
||||
modelo: modelo ?? null,
|
||||
board: 'OPORTUNIDADES',
|
||||
etapa: 'Afluencia',
|
||||
datosJson: JSON.stringify(datos),
|
||||
pendienteSync: true,
|
||||
},
|
||||
})
|
||||
return { id: oportunidadId, oportunidadId, actualizada: true }
|
||||
throw new NotFoundError(`No existe la oportunidad ${oportunidadId}`)
|
||||
}
|
||||
|
||||
// Sólo se pisan los campos capturados: lo que no viene en el payload se
|
||||
|
||||
@@ -137,6 +137,8 @@ describe('obtenerScores', () => {
|
||||
where: {
|
||||
board: { in: ['LEADS', 'OPORTUNIDADES'] },
|
||||
usuarioId: 'u1',
|
||||
// Los cerrados (descartados/perdidos) no reciben score
|
||||
etapa: { notIn: ['Descartado', 'Perdido'] },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,9 +10,10 @@ import { prisma } from './prisma.js'
|
||||
|
||||
const ETAPA_SCORE: Record<string, number> = {
|
||||
NUEVO: 20,
|
||||
'INTENTO DE CONTACTO': 30,
|
||||
CONTACTADO: 40,
|
||||
INTERESADO: 60,
|
||||
AGENDO: 80,
|
||||
'AGENDO CITA': 80,
|
||||
CALIFICADO: 90,
|
||||
}
|
||||
|
||||
@@ -74,6 +75,8 @@ export function calcularScore(lead: {
|
||||
export async function obtenerScores(usuarioId?: string): Promise<LeadScore[]> {
|
||||
const where: Record<string, unknown> = {
|
||||
board: { in: ['LEADS', 'OPORTUNIDADES'] },
|
||||
// Los cerrados (descartados/perdidos) no reciben score ni salen en el ranking
|
||||
etapa: { notIn: ['Descartado', 'Perdido'] },
|
||||
}
|
||||
if (usuarioId) {
|
||||
where['usuarioId'] = usuarioId
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// ---------- Prisma mockeado ----------
|
||||
// La pasada consulta un admin de respaldo para los planes sin vendedor
|
||||
// (antes caían a 'sin-destinatario' y nadie los veía).
|
||||
const prismaMock = vi.hoisted(() => ({ usuario: { findFirst: vi.fn() } }))
|
||||
vi.mock('./prisma.js', () => ({ prisma: prismaMock }))
|
||||
|
||||
import type { Pago, PlanPago } from './cobranza.service.js'
|
||||
import {
|
||||
RecordatoriosCobranzaService,
|
||||
@@ -231,7 +238,29 @@ describe('RecordatoriosCobranzaService.ejecutarPasada', () => {
|
||||
expect(log).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('sin destinatario (plan sin vendedor): solo registra en el log, sin canal', async () => {
|
||||
it('sin destinatario (plan sin vendedor): el recordatorio cae al administrador de respaldo', async () => {
|
||||
prismaMock.usuario.findFirst.mockResolvedValue({ id: 'admin-qa' })
|
||||
const planesSinVendedor = [
|
||||
plan({
|
||||
id: 'p1',
|
||||
vendedorId: null,
|
||||
pagos: [pago({ id: 'pg1', fechaVencimiento: isoMasDias(HOY, 2) })],
|
||||
}),
|
||||
]
|
||||
const { store, log } = storeFake(planesSinVendedor)
|
||||
const { canal, envios } = canalFake()
|
||||
const svc = new RecordatoriosCobranzaService(store, [canal])
|
||||
|
||||
const r = await svc.ejecutarPasada({ ahora: HOY })
|
||||
expect(r.enviados).toHaveLength(1)
|
||||
expect(r.enviados[0]).toMatchObject({ canal: 'fake', destinatario: 'admin-qa' })
|
||||
expect(envios).toHaveLength(1)
|
||||
expect(envios[0].destinatario).toMatchObject({ vendedorId: 'admin-qa', origen: 'admin-fallback' })
|
||||
expect(log).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('sin destinatario y SIN admin en el sistema: solo registra en el log, sin canal', async () => {
|
||||
prismaMock.usuario.findFirst.mockResolvedValue(null)
|
||||
const planesSinVendedor = [
|
||||
plan({
|
||||
id: 'p1',
|
||||
|
||||
@@ -130,10 +130,11 @@ export function debeEnviar(
|
||||
export interface DestinatarioRecordatorio {
|
||||
/** usuarioId del vendedor responsable; null = no hay a quién asignar */
|
||||
vendedorId: string | null
|
||||
origen: 'vendedor' | 'ninguno'
|
||||
origen: 'vendedor' | 'ninguno' | 'admin-fallback'
|
||||
}
|
||||
|
||||
/** Regla pura de selección: vendedor del plan → nadie */
|
||||
/** Regla pura de selección: vendedor del plan → nadie (el fallback a admin se
|
||||
* resuelve en la pasada, que tiene acceso a la BD). */
|
||||
export function elegirDestinatario(vendedorId: string | null): DestinatarioRecordatorio {
|
||||
if (vendedorId !== null) return { vendedorId, origen: 'vendedor' }
|
||||
return { vendedorId: null, origen: 'ninguno' }
|
||||
@@ -218,9 +219,21 @@ export class RecordatoriosCobranzaService {
|
||||
const planes = await this.store.cargarPlanes()
|
||||
const candidatos = candidatosRecordatorio(planes, ahora, diasAntes)
|
||||
|
||||
// Planes sin vendedor asignado (p. ej. cartera importada sin asesor): el
|
||||
// recordatorio cae al administrador para que no se pierda y pueda
|
||||
// reasignar el plan. Antes quedaban en 'sin-destinatario' y nadie los veía.
|
||||
const adminFallback = await prisma.usuario.findFirst({
|
||||
where: { rol: 'admin', activo: true },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
select: { id: true },
|
||||
})
|
||||
|
||||
const resultado: ResultadoPasada = { dryRun, evaluados: [], enviados: [], fallidos: [] }
|
||||
for (const c of candidatos) {
|
||||
const destinatario = elegirDestinatario(c.vendedorId)
|
||||
let destinatario = elegirDestinatario(c.vendedorId)
|
||||
if (destinatario.origen === 'ninguno' && adminFallback) {
|
||||
destinatario = { vendedorId: adminFallback.id, origen: 'admin-fallback' }
|
||||
}
|
||||
|
||||
const ultimo = await this.store.ultimoRecordatorio(c.pagoId, c.tipo)
|
||||
if (!debeEnviar(ultimo, c.tipo, ahora, revolverDias)) {
|
||||
|
||||
@@ -77,15 +77,17 @@ describe('construirMiTablero', () => {
|
||||
captura({ board: 'LEADS' }),
|
||||
captura({ board: 'LEADS' }),
|
||||
captura({ board: 'OPORTUNIDADES' }),
|
||||
captura({ board: 'APARTADO', etapa: 'FIRMA CLIENTE (VENTA)' }),
|
||||
captura({ board: 'APARTADO', etapa: 'CANCELADO' }),
|
||||
captura({ board: 'APARTADO', etapa: 'FIRMA CLIENTE' }),
|
||||
captura({ board: 'APARTADO', etapa: 'CANCELO' }),
|
||||
captura({ board: 'APARTADO', etapa: 'ESCRITURADO' }),
|
||||
captura({ board: 'LEADS', etapa: 'DESCARTADO' }),
|
||||
]
|
||||
const tablero = construirMiTablero(capturas, [], [], AHORA)
|
||||
expect(tablero.conversion.leads).toBe(3) // 2 activos + 1 descartado
|
||||
expect(tablero.conversion.oportunidades).toBe(1)
|
||||
expect(tablero.conversion.apartados).toBe(1) // CANCELADO no cuenta
|
||||
expect(tablero.conversion.vendidos).toBe(1)
|
||||
// Cancelo no cuenta; Firma Cliente y Escriturado ya son ventas
|
||||
expect(tablero.conversion.apartados).toBe(0)
|
||||
expect(tablero.conversion.vendidos).toBe(2)
|
||||
expect(tablero.conversion.descartados).toBe(1) // etapa terminal DESCARTADO
|
||||
})
|
||||
|
||||
@@ -146,9 +148,9 @@ describe('construirMiTablero', () => {
|
||||
|
||||
it('agrupa por desarrollo solo con vendidos', () => {
|
||||
const capturas = [
|
||||
captura({ board: 'APARTADO', etapa: 'FIRMA CLIENTE (VENTA)', desarrollo: 'DIAGONAL' }),
|
||||
captura({ board: 'APARTADO', etapa: 'FIRMA CLIENTE', desarrollo: 'DIAGONAL' }),
|
||||
captura({ board: 'APARTADO', etapa: 'ESCRITURADO', desarrollo: 'DIAGONAL' }),
|
||||
captura({ board: 'APARTADO', etapa: 'FIRMA CLIENTE (VENTA)', desarrollo: 'ATELIER' }),
|
||||
captura({ board: 'APARTADO', etapa: 'FIRMA CLIENTE', desarrollo: 'ATELIER' }),
|
||||
captura({ board: 'APARTADO', etapa: 'NUEVO', desarrollo: 'SENDA' }), // no vendido
|
||||
]
|
||||
const tablero = construirMiTablero(capturas, [], [], AHORA)
|
||||
|
||||
@@ -122,13 +122,19 @@ export function construirMiTablero(
|
||||
// -- Conversión (funnel) --
|
||||
const leads = capturas.filter((c) => c.board === 'LEADS').length
|
||||
const oportunidades = capturas.filter((c) => c.board === 'OPORTUNIDADES').length
|
||||
// Apartados ACTIVOS del funnel: ni cancelados (Cancelo/Cancelacion Apartado)
|
||||
// ni ya vendidos (Firma Cliente/Escriturado/Titulado pasan a "vendidos")
|
||||
const apartados = capturas.filter(
|
||||
(c) => c.board === 'APARTADO' && c.etapa.toUpperCase() !== 'CANCELADO',
|
||||
(c) =>
|
||||
c.board === 'APARTADO' &&
|
||||
!['FIRMA CLIENTE', 'CANCELO', 'CANCELACION APARTADO', 'ESCRITURADO', 'TITULADO'].includes(
|
||||
c.etapa.toUpperCase(),
|
||||
),
|
||||
).length
|
||||
const vendidos = capturas.filter(
|
||||
(c) =>
|
||||
c.board === 'APARTADO' &&
|
||||
['FIRMA CLIENTE (VENTA)', 'ESCRITURADO', 'FORMALIZADO'].includes(c.etapa.toUpperCase()),
|
||||
['FIRMA CLIENTE', 'ESCRITURADO', 'TITULADO'].includes(c.etapa.toUpperCase()),
|
||||
).length
|
||||
const descartados = capturas.filter((c) => c.etapa.toUpperCase() === 'DESCARTADO').length
|
||||
|
||||
@@ -184,7 +190,7 @@ export function construirMiTablero(
|
||||
for (const c of capturas) {
|
||||
if (
|
||||
c.board === 'APARTADO' &&
|
||||
['FIRMA CLIENTE (VENTA)', 'ESCRITURADO', 'FORMALIZADO'].includes(c.etapa.toUpperCase()) &&
|
||||
['FIRMA CLIENTE', 'ESCRITURADO', 'TITULADO'].includes(c.etapa.toUpperCase()) &&
|
||||
c.desarrollo
|
||||
) {
|
||||
const dev = c.desarrollo.trim()
|
||||
|
||||
Reference in New Issue
Block a user