feat(pendientes): cotizador (mensualidades, bloqueo de unidad, totales, numMensualidades), cobranza (recibos, atrasos, mora configurable, recordatorios), soporte (adjuntos, filtros, estados), clientes maestro (catalogos reales), contactos (ficha y duplicados) y permisos de captura por rol

This commit is contained in:
2026-09-21 08:34:53 +00:00
parent b5a20aed4e
commit 2fc80e7f9e
66 changed files with 3792 additions and 436 deletions

View File

@@ -344,8 +344,16 @@ export class CrmLocalService {
* oportunidad pasan a apuntar al apartado para que sobrevivan al cambio de
* board. Idempotente con el mismo dedup `apartado-creado:<opId>` de las
* automatizaciones.
*
* `opciones.unidadCodigo` deja la unidad apartada en el `datosJson` del
* apartado (la cotización del vendedor o la unidad de interés): es lo que
* después leen el bloqueo de inventario y Mi día.
*/
async apartar(oportunidadIdLocal: string, usuarioId?: string) {
async apartar(
oportunidadIdLocal: string,
usuarioId?: string,
opciones: { unidadCodigo?: string | undefined } = {},
) {
// Sombra para ids heredados sin fila local
const oportunidad = await this.filaLocal(
oportunidadIdLocal,
@@ -362,6 +370,7 @@ export class CrmLocalService {
}
const apartadoId = idLocal()
const datosJson = conUnidadCodigo(oportunidad.datosJson, opciones.unidadCodigo)
const resultados = await prisma.$transaction([
prisma.leadCaptura.update({
where: { leadId: oportunidadIdLocal },
@@ -379,11 +388,11 @@ export class CrmLocalService {
modelo: oportunidad.modelo,
board: 'APARTADO',
etapa: 'Pago Apartado',
datosJson: oportunidad.datosJson,
datosJson,
// Fecha límite del apartado en la COLUMNA: la leen el KPI "Apartados
// por vencer", el cron de avisos y la lista de Mi día (antes sólo se
// calculaba en el JSON del alta y la columna quedaba vacía).
fechaLimite: fechaLimiteApartado(oportunidad.datosJson),
fechaLimite: fechaLimiteApartado(datosJson),
pendienteSync: true,
},
}),
@@ -472,6 +481,25 @@ function soloDigitos(telefono: string): string {
return telefono.replace(/\D/g, '')
}
/**
* `datosJson` con la unidad apartada dentro (mezcla, no reemplaza: conserva
* fechaCita, fechaLimite y el perfilamiento de la captura). Sin unidadCodigo
* devuelve el JSON original tal cual.
*/
function conUnidadCodigo(datosJson: string | null, unidadCodigo?: string): string | null {
const unidad = unidadCodigo?.trim()
if (!unidad) return datosJson
let datos: Record<string, unknown> = {}
if (datosJson) {
try {
datos = JSON.parse(datosJson) as Record<string, unknown>
} catch {
datos = {}
}
}
return JSON.stringify({ ...datos, unidadCodigo: unidad })
}
/**
* Busca un contacto por teléfono (últimos 10 dígitos) en CUALQUIER tablero.
* Así el mismo número no se duplica entre Leads, Oportunidades y Apartados.