feat(contactos): detalle completo del cliente con propiedad e historial
- GET /api/contactos/:id/detalle: todas las columnas del contacto, la propiedad comprada/en proceso (apartado relacionado por teléfono/email, enriquecido con el desarrollo de la unidad), quién atiende cada item relacionado (leads/oportunidades/apartados, columna people) y el historial completo de atención (activity log de los 4 tableros + capturas y alta local con nombre del capturista) - monday.service: getContactoDetalle (columnas por título), getHistorialItems (todos los eventos del activity log por items) y ApartadoMonday enriquecido (teléfono, email, precio final, fecha compra) - Landing: la lista de Contactos abre un modal por cliente con datos, propiedad, relaciones CRM (con quién atiende) y timeline de historial - Tests: 465 API + 12 landing, verde
This commit is contained in:
@@ -80,6 +80,10 @@ export interface ApartadoMonday {
|
||||
fechaLimite: string // 'YYYY-MM-DD' de 'Fecha límite' ('' si no tiene)
|
||||
estado: string // label de la columna status 'Estado'
|
||||
vendedorIds: number[] // user IDs asignados en la columna people 'Vendedor'
|
||||
telefono?: string // columna phone 'Teléfono' ('' si no tiene)
|
||||
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)
|
||||
}
|
||||
|
||||
/** Contacto del tablero Contactos (módulo Contactos de la landing) */
|
||||
@@ -136,6 +140,14 @@ interface ActivityLogMonday {
|
||||
data: string // JSON string; pulse_id identifica el item
|
||||
}
|
||||
|
||||
/** Evento del activity log de un item (historial completo, no solo el último) */
|
||||
export interface EventoHistorialMonday {
|
||||
itemId: string
|
||||
fechaISO: string
|
||||
usuarioId: number | null // user_id de Monday; null si no es numérico (automatizaciones)
|
||||
evento: string // event crudo (create_pulse, update_column_value, ...)
|
||||
}
|
||||
|
||||
/** Último evento del activity log de un item (para "Último cambio" de Mi día) */
|
||||
export interface UltimoCambioMonday {
|
||||
fechaISO: string
|
||||
@@ -1046,6 +1058,59 @@ export class MondayService {
|
||||
return resultado
|
||||
}
|
||||
|
||||
/**
|
||||
* TODOS los eventos del activity log de un conjunto de items (historial del
|
||||
* módulo Contactos), más reciente primero. Misma normalización de created_at
|
||||
* que getUltimosCambios.
|
||||
*/
|
||||
async getHistorialItems(boardId: string, itemIds: string[]): Promise<EventoHistorialMonday[]> {
|
||||
if (itemIds.length === 0) return []
|
||||
const data = await this.graphql<{ boards: { activity_logs: ActivityLogMonday[] }[] }>(
|
||||
`query ($ids: [ID!], $itemIds: [ID!], $limit: Int!) {
|
||||
boards(ids: $ids) {
|
||||
activity_logs(item_ids: $itemIds, limit: $limit) { id event created_at user_id data }
|
||||
}
|
||||
}`,
|
||||
{ ids: [boardId], itemIds, limit: Math.min(itemIds.length * 10, 200) },
|
||||
)
|
||||
const eventos: EventoHistorialMonday[] = []
|
||||
for (const log of data.boards[0]?.activity_logs ?? []) {
|
||||
let itemId: string | undefined
|
||||
try {
|
||||
const datos = JSON.parse(log.data) as { pulse_id?: number | string }
|
||||
itemId = datos.pulse_id === undefined ? undefined : String(datos.pulse_id)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
if (itemId === undefined) continue
|
||||
const crudo = Number(log.created_at)
|
||||
if (!Number.isFinite(crudo)) continue
|
||||
const ms = crudo > 1e16 ? crudo / 1e4 : crudo > 1e14 ? crudo / 1e3 : crudo
|
||||
const userId = Number(log.user_id)
|
||||
eventos.push({
|
||||
itemId,
|
||||
fechaISO: new Date(ms).toISOString(),
|
||||
usuarioId: Number.isFinite(userId) && userId > 0 ? userId : null,
|
||||
evento: log.event,
|
||||
})
|
||||
}
|
||||
return eventos.sort((a, b) => b.fechaISO.localeCompare(a.fechaISO))
|
||||
}
|
||||
|
||||
/** Item del tablero Contactos con TODAS sus columnas por título ('' si vacía) */
|
||||
async getContactoDetalle(itemId: string): Promise<Record<string, string> | null> {
|
||||
const boardId = config.monday.boards.contactos
|
||||
const [cols, item] = await Promise.all([this.columnasDe(boardId), this.getItemConGrupo(itemId)])
|
||||
if (!item) return null
|
||||
const porTitulo: Record<string, string> = { nombre: item.name }
|
||||
for (const [titulo, col] of cols) {
|
||||
const cv = item.column_values.find((c) => c.id === col.id)
|
||||
// status nunca escrito: value null pero text muestra el default del tablero
|
||||
porTitulo[titulo] = cv?.value ? (cv.text ?? '') : ''
|
||||
}
|
||||
return porTitulo
|
||||
}
|
||||
|
||||
/** Parsea el value JSON de una columna people a los user IDs (solo personas) */
|
||||
private peopleIds(raw: string | null | undefined): number[] {
|
||||
if (!raw) return []
|
||||
@@ -1121,6 +1186,10 @@ export class MondayService {
|
||||
const colFechaLimite = cols.get('Fecha límite')
|
||||
const colEstado = cols.get('Estado')
|
||||
const colVendedor = cols.get('Vendedor')
|
||||
const colTelefono = cols.get('Teléfono')
|
||||
const colEmail = cols.get('Correo electrónico')
|
||||
const colPrecioFinal = cols.get('Precio Final')
|
||||
const colFechaCompra = cols.get('Fecha Compra')
|
||||
|
||||
const items: ItemMonday[] = []
|
||||
let cursor: string | null = null
|
||||
@@ -1168,6 +1237,10 @@ export class MondayService {
|
||||
fechaLimite: colFechaLimite ? (porId.get(colFechaLimite.id)?.text ?? '') : '',
|
||||
estado,
|
||||
vendedorIds: colVendedor ? this.peopleIds(porId.get(colVendedor.id)?.value) : [],
|
||||
telefono: colTelefono ? (porId.get(colTelefono.id)?.text ?? '') : '',
|
||||
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 ?? '') : '',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user