feat: conversión lead→oportunidad (Agendo) y alta ligada a apartado

- Mis leads: botón 'Agendo →' convierte el lead en oportunidad (etapa
  Agendo Cita, con sus datos, asesor = vendedor del lead o quien
  convierte), marca el lead Calificado y guarda el mapping
  LeadOportunidad; 409 si ya fue convertido
- Mis oportunidades: se quitan No Asistio y Abandono (redundantes con
  Descartar); quedan Cotizar, Afluencia, Agendo Cita y Aparto
- Mis apartados: 'Firma cliente (venta) →' abre el alta de cliente con
  TODO lo recolectado prellenado (ficha del apartado + contacto +
  oportunidad + cotización vinculada vía GET /altas/prefill-apartado/:id)
- El alta con apartadoMondayId se LIGA al apartado existente y lo
  enriquece (curp, teléfono, email, precio final, enganche, forma de
  pago, unidad) en vez de crear un duplicado
- monday.service: actualizarApartado + ApartadoMonday con curp, forma de
  pago, enganche y monto apartado
- Tests: 470 API + 120 alta-clientes + 16 mi-dia, verde
This commit is contained in:
2026-09-05 00:33:53 +00:00
parent a4575bb948
commit 5b348d21e5
14 changed files with 654 additions and 17 deletions

View File

@@ -84,6 +84,10 @@ export interface ApartadoMonday {
email?: string // columna email 'Correo electrónico' ('' si no tiene)
precioFinal?: number // columna numbers 'Precio Final' (0 si no tiene)
fechaCompra?: string // 'YYYY-MM-DD' de 'Fecha Compra' ('' si no tiene)
curp?: string // columna text 'curp' ('' si no tiene)
formaPago?: string // dropdown 'Forma de Pago' ('' si no tiene)
enganche?: number // columna numbers 'Enganche' (0 si no tiene)
montoApartado?: number // columna numbers 'Monto apartado' (0 si no tiene)
}
/** Contacto del tablero Contactos (módulo Contactos de la landing) */
@@ -510,13 +514,15 @@ export class MondayService {
* Oportunidad en etapa 'Afluencia' con todo el perfil de captura (la
* afluencia ES la oportunidad: no hay item aparte en el tablero Leads).
* La columna date 'Fecha' lleva la fecha de captura (hoy, hora local).
* estadoLabel permite crearla en otra etapa (p. ej. 'Agendo Cita' al
* convertir un lead del tablero Leads).
*/
async crearOportunidadAfluencia(datos: DatosOportunidadAfluencia): Promise<string> {
const boardId = config.monday.boards.oportunidades
const hoy = new Date()
const fechaHoy = `${hoy.getFullYear()}-${String(hoy.getMonth() + 1).padStart(2, '0')}-${String(hoy.getDate()).padStart(2, '0')}`
const columnValues = await this.construirColumnValues(boardId, {
Estado: 'Afluencia',
Estado: datos.estadoLabel ?? 'Afluencia',
Fecha: fechaHoy,
'Cliente teléfono': datos.telefono,
'Cliente email': datos.email,
@@ -682,6 +688,41 @@ export class MondayService {
return this.crearItem(boardId, datos.clienteNombre, columnValues)
}
/**
* Actualiza un apartado existente con los campos presentes (mismo builder de
* valores que crearApartado) — usado cuando el alta de cliente arranca desde
* un apartado ya creado (botón "Firma Cliente (Venta)" de Mis apartados):
* se enriquece el item en vez de crear uno duplicado.
*/
async actualizarApartado(itemId: string, datos: Partial<DatosApartado>): Promise<void> {
const boardId = config.monday.boards.apartado
const valores = JSON.parse(
await this.construirColumnValues(boardId, {
Unidad: datos.unidadCodigo,
'Monto apartado': datos.montoApartado,
'Fecha apartado': datos.fechaApartado,
'Fecha límite': datos.fechaLimite,
Estado: datos.estadoLabel,
Teléfono: datos.telefono,
'Correo electrónico': datos.email,
curp: datos.curp,
'Precio Final': datos.precioFinal,
Enganche: datos.enganche,
'Fecha Compra': datos.fechaCompra,
Vendedor: datos.vendedorId,
'Forma de Pago': datos.formaPagoLabel,
Observaciones: datos.observaciones,
}),
) as Record<string, unknown>
if (datos.clienteNombre !== undefined && datos.clienteNombre !== '') valores['name'] = datos.clienteNombre
await this.graphql(
`mutation ($boardId: ID!, $itemId: ID!, $columnValues: JSON!) {
change_multiple_column_values(board_id: $boardId, item_id: $itemId, column_values: $columnValues) { id }
}`,
{ boardId, itemId, columnValues: JSON.stringify(valores) },
)
}
async actualizarDisponibilidadUnidad(codigo: string, status: 'DISPONIBLE' | 'APARTADA' | 'VENDIDA'): Promise<void> {
const boardId = config.monday.boards.unidades
// Asegura cache fresco para resolver el itemId
@@ -1190,6 +1231,10 @@ export class MondayService {
const colEmail = cols.get('Correo electrónico')
const colPrecioFinal = cols.get('Precio Final')
const colFechaCompra = cols.get('Fecha Compra')
const colCurp = cols.get('curp')
const colFormaPago = cols.get('Forma de Pago')
const colEnganche = cols.get('Enganche')
const colMontoApartado = cols.get('Monto apartado')
const items: ItemMonday[] = []
let cursor: string | null = null
@@ -1241,6 +1286,10 @@ export class MondayService {
email: colEmail ? (porId.get(colEmail.id)?.text ?? '') : '',
precioFinal: colPrecioFinal ? Number(porId.get(colPrecioFinal.id)?.text ?? 0) || 0 : 0,
fechaCompra: colFechaCompra ? (porId.get(colFechaCompra.id)?.text ?? '') : '',
curp: colCurp ? (porId.get(colCurp.id)?.text ?? '') : '',
formaPago: colFormaPago ? (porId.get(colFormaPago.id)?.text ?? '') : '',
enganche: colEnganche ? Number(porId.get(colEnganche.id)?.text ?? 0) || 0 : 0,
montoApartado: colMontoApartado ? Number(porId.get(colMontoApartado.id)?.text ?? 0) || 0 : 0,
}
})
}
@@ -1515,6 +1564,8 @@ export interface DatosOportunidad {
export interface DatosOportunidadAfluencia {
nombre: string // name del item = nombre del interesado
/** Etapa inicial (default 'Afluencia'; 'Agendo Cita' al convertir un lead) */
estadoLabel?: string | undefined
telefono?: string | undefined
email?: string | undefined
desarrolloLabel?: string | undefined // label tal cual de la columna status 'Desarrollo'