feat(mi-dia): vendedores solo-app con cuenta Monday compartida

- Las listas personales de Mi día (leads, oportunidades, apartados) se
  calculan por captura local (LeadCaptura/Cliente + mapping
  LeadOportunidad) ADEMÁS de la columna people de Monday: la app ya es
  totalmente usable sin cuenta Monday
- Config MONDAY_VENDEDOR_COMPARTIDO_ID: cuando el usuario está ligado a
  la cuenta compartida, la columna people se ignora para 'lo mío' (si
  no, todos verían los leads de todos)
- scripts/vincular-vendedores-compartido.js: vincula todos los usuarios
  con rol vendedor a la cuenta Monday compartida
- Tests: 478 API, verde; verificado en vivo con usuario sin Monday
This commit is contained in:
2026-09-05 05:37:52 +00:00
parent b40b8d4627
commit af2fb10956
6 changed files with 171 additions and 24 deletions

View File

@@ -208,10 +208,27 @@ export async function resumenMiDia(
createdAt: Date
}[] = [],
ahora: Date = new Date(),
/** User ID de la cuenta Monday COMPARTIDA de vendedores (config); con ella la
* columna people ya no distingue personas y "lo mío" viene de la captura local */
compartidoId: number | null = null,
/** ids de items Monday capturados por el usuario (leads + oportunidades
* mapeadas); default: los leadId de `capturas` */
capturadosIds?: Set<string>,
): Promise<ResumenMiDia> {
const miId = usuario.mondayUserId
const sinMapeo = miId === null
// "Mío" = lo que capturé en la app (registro local) o lo que Monday me tiene
// asignado (columna people). Con la cuenta compartida, people ya no distingue
// personas: solo cuenta la captura local. Funciona SIN vínculo Monday.
const idsCapturados = capturadosIds ?? new Set(capturas.map((c) => c.leadId))
const esCompartido = miId !== null && compartidoId !== null && compartidoId > 0 && miId === compartidoId
const esMio = (peopleIds: number[], itemId: string): boolean => {
if (idsCapturados.has(itemId)) return true
if (esCompartido || miId === null) return false
return peopleIds.includes(miId)
}
const [leads, oportunidades, actividades, apartados] = await Promise.all([
monday.getLeads(),
monday.getOportunidades(),
@@ -224,26 +241,26 @@ export async function resumenMiDia(
const en7Dias = fechaLocal(ahora, 7)
const hace7DiasMs = ahora.getTime() - 7 * MS_DIA
// --- Listas personales (vacías si no hay mapeo Monday) ---
// "Mis leads" mezcla ambos tableros: Leads (Vendedor = yo, estado no
// terminal) + Oportunidades (Asesor = yo, etapa no terminal), más reciente
// primero y con su fuente para las acciones del CRM.
const misLeadsLeads: LeadMiDia[] = sinMapeo
? []
: leads
.filter((l) => l.vendedorIds.includes(miId) && !esEstadoTerminalLead(l.estado))
.map((l) => ({
id: l.id,
nombre: l.nombre,
telefono: l.telefono,
email: l.email,
estado: l.estado,
origen: l.origen,
updatedAt: l.updatedAt,
fuente: 'lead',
ultimoCambio: null,
}))
const mios = sinMapeo ? [] : oportunidades.filter((o) => o.asesorIds.includes(miId))
// --- Listas personales (por captura local + asignación Monday; ver esMio) ---
// "Mis leads" mezcla AMBOS tableros del embudo: Leads (captura ligera, columna
// Vendedor) y Oportunidades (Alta Afluencia, columna Asesor — la afluencia ES
// la oportunidad en etapa 'Afluencia'). "Mis capturas" viene del registro
// local (tabla LeadCaptura), no de Monday: es la lista de items que el usuario
// dio de alta, con su estado actual (cruza contra ambos tableros por id).
const misLeadsLeads: LeadMiDia[] = leads
.filter((l) => esMio(l.vendedorIds, l.id) && !esEstadoTerminalLead(l.estado))
.map((l) => ({
id: l.id,
nombre: l.nombre,
telefono: l.telefono,
email: l.email,
estado: l.estado,
origen: l.origen,
updatedAt: l.updatedAt,
fuente: 'lead',
ultimoCambio: null,
}))
const mios = oportunidades.filter((o) => esMio(o.asesorIds, o.id))
const misLeadsOportunidades: LeadMiDia[] = mios
.filter((o) => !esEtapaTerminal(o.etapa))
.map((o) => ({
@@ -280,9 +297,15 @@ export async function resumenMiDia(
}))
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
const miosApartados = sinMapeo
? []
: apartados.filter((a) => a.vendedorIds.includes(miId) && !esEstadoFinalApartado(a.estado))
// Apartados ligados a MIS altas de cliente (registro local): con la cuenta
// compartida o sin vínculo Monday, esta liga local es la que personaliza
const apartadosDeMisClientes = new Set(
clientes.map((c) => c.mondayApartadoId).filter((id): id is string => id !== null),
)
const miosApartados = apartados.filter(
(a) =>
(esMio(a.vendedorIds, a.id) || apartadosDeMisClientes.has(a.id)) && !esEstadoFinalApartado(a.estado),
)
const [cambiosLeads, cambiosOportunidades, cambiosApartados] = await Promise.all([
monday.getUltimosCambios(
@@ -414,7 +437,7 @@ export async function resumenMiDia(
}))
// Conteo de la semana: míos en AMBOS tableros (leads + oportunidades)
const miosLeadRows = sinMapeo ? [] : leads.filter((l) => l.vendedorIds.includes(miId))
const miosLeadRows = leads.filter((l) => esMio(l.vendedorIds, l.id))
const conteos = {
leadsNuevosSemana:
miosLeadRows.filter((l) => Date.parse(l.createdAt) >= hace7DiasMs).length +