feat(round-robin): reparto solo con el equipo 'vendedores' de Monday

- obtenerVendedores ahora lee el team de Monday (MONDAY_RR_TEAM,
  default 'vendedores') con cache de 1 h; MONDAY_RR_USER_IDS queda como
  override manual.
- Escrituras a columnas status resuelven el label real sin importar
  mayusculas: si renombran labels en la UI de Monday (paso con
  NUEVO→Nuevo) el API sigue escribiendo en vez de tronar con 502.
- mapearLead: un status nunca escrito (value null) se lee como vacio;
  Monday regresaba el label default del tablero como texto y el
  round-robin creia que el lead estaba descartado.
This commit is contained in:
2026-09-04 02:39:37 +00:00
parent f382987fbf
commit d4fc9ee892
5 changed files with 127 additions and 15 deletions

View File

@@ -370,6 +370,23 @@ export class MondayService {
}
}
/**
* Resuelve el label real de una columna status tolerando mayúsculas y
* minúsculas (el código escribe 'NUEVO' aunque el board diga 'Nuevo' tras
* un rename en la UI). Sin match devuelve el valor tal cual (Monday dirá
* que el label no existe, como antes).
*/
private async resolverLabelStatus(
boardId: string,
col: ColumnaMeta,
valor: string | number | boolean,
): Promise<string | number | boolean> {
if (col.type !== 'status') return valor
const buscado = String(valor).trim().toUpperCase()
const labels = await this.labelsDeColumna(boardId, col.title)
return labels.find((l) => l.trim().toUpperCase() === buscado) ?? valor
}
private async construirColumnValues(
boardId: string,
valores: Record<string, string | number | boolean | undefined>,
@@ -378,7 +395,7 @@ export class MondayService {
for (const [titulo, crudo] of Object.entries(valores)) {
if (crudo === undefined || crudo === '') continue
const col = await this.columnaId(boardId, titulo)
out[col.id] = this.valorPorTipo(col, crudo)
out[col.id] = this.valorPorTipo(col, await this.resolverLabelStatus(boardId, col, crudo))
}
return JSON.stringify(out)
}
@@ -544,11 +561,12 @@ export class MondayService {
valor: string | number | boolean,
): Promise<void> {
const col = await this.columnaId(boardId, tituloColumna)
const resuelto = await this.resolverLabelStatus(boardId, col, valor)
await this.graphql(
`mutation ($boardId: ID!, $itemId: ID!, $columnId: String!, $value: JSON!) {
change_column_value(board_id: $boardId, item_id: $itemId, column_id: $columnId, value: $value) { id }
}`,
{ boardId, itemId, columnId: col.id, value: JSON.stringify(this.valorPorTipo(col, valor)) },
{ boardId, itemId, columnId: col.id, value: JSON.stringify(this.valorPorTipo(col, resuelto)) },
)
}
@@ -706,6 +724,11 @@ export class MondayService {
const colOrigen = cols.get('Origen')
const colTelefono = cols.get('Teléfono')
const colEmail = cols.get('Email')
// Cuando el status nunca se ha escrito, Monday devuelve value=null pero
// `text` muestra el label default del tablero (hoy "Descartado"): el estado
// real es vacío, no el default. Se lee `text` SOLO si hay value.
const estadoCv = colEstado ? porId.get(colEstado.id) : undefined
const estado = estadoCv?.value ? (estadoCv.text ?? '') : ''
// Perfilamiento: lectura tolerante por título ('' / null si falta la columna)
const textoDe = (titulo: string): string => {
const col = cols.get(titulo)
@@ -718,7 +741,7 @@ export class MondayService {
nombre: item.name,
createdAt: item.created_at,
updatedAt: item.updated_at,
estado: colEstado ? (porId.get(colEstado.id)?.text ?? '') : '',
estado,
origen: colOrigen ? (porId.get(colOrigen.id)?.text ?? '') : '',
telefono: colTelefono ? (porId.get(colTelefono.id)?.text ?? '') : '',
email: colEmail ? (porId.get(colEmail.id)?.text ?? '') : '',
@@ -1083,6 +1106,23 @@ export class MondayService {
.filter((u) => !u.is_guest && u.enabled && !u.email.endsWith('@agent.monday.com'))
.map((u) => ({ id: Number(u.id), nombre: u.name, email: u.email }))
}
/**
* Miembros de un equipo (team) de Monday por nombre, case-insensitive
* (p. ej. "vendedores" para el round-robin). Mismo filtro que el workspace.
*/
async getUsuariosEquipo(nombre: string): Promise<UsuarioMonday[]> {
const data = await this.graphql<{
teams: { name: string; users: { id: string; name: string; email: string; is_guest: boolean; enabled: boolean }[] }[] | null
}>(`query { teams { name users { id name email is_guest enabled } } }`)
const equipo = (data.teams ?? []).find(
(t) => t.name.trim().toUpperCase() === nombre.trim().toUpperCase(),
)
if (!equipo) throw new AppError(500, `El equipo "${nombre}" no existe en Monday`)
return equipo.users
.filter((u) => !u.is_guest && u.enabled && !u.email.endsWith('@agent.monday.com'))
.map((u) => ({ id: Number(u.id), nombre: u.name, email: u.email }))
}
}
// ---------- DTOs de escritura ----------