feat(purga): Monday eliminado completamente del sistema

14 servicios eliminados, 7 controllers local-only, crons locales,
config sin MONDAY_*, frontend sin banners/avisos Monday.
552 tests API + 7 apps construidas.
This commit is contained in:
2026-09-11 11:47:17 +00:00
parent 64b01246d6
commit 89d95eb53b
130 changed files with 2018 additions and 14747 deletions

View File

@@ -1,32 +0,0 @@
import { useEffect, useState } from 'react'
import { fetchEstadoSistema } from '../services/sistema'
const INTERVALO_POLL_MS = 60_000
export function useModoLocal(): boolean {
const [modoLocal, setModoLocal] = useState(false)
useEffect(() => {
let activo = true
const consultar = () => {
void fetchEstadoSistema().then((estado) => {
if (activo) setModoLocal(estado !== null && !estado.mondayOk)
})
}
consultar()
const id = setInterval(consultar, INTERVALO_POLL_MS)
return () => { activo = false; clearInterval(id) }
}, [])
return modoLocal
}
export default function BannerModoLocal() {
const modoLocal = useModoLocal()
if (!modoLocal) return null
return (
<div role="status" className="border-b border-amber-200 bg-amber-50">
<p className="mx-auto max-w-[1280px] px-4 py-2 text-xs font-medium text-amber-700 sm:px-6">
Modo local — Monday no disponible. Tus capturas se guardan y se sincronizarán automáticamente.
</p>
</div>
)
}

View File

@@ -1,6 +1,6 @@
// @vitest-environment jsdom
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { ConfiguracionCuenta } from './ConfiguracionCuenta'
type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
@@ -20,11 +20,6 @@ function crearStorageStub(): Storage {
} as Storage
}
const USUARIOS_MONDAY = [
{ id: 13676, nombre: 'Erick Peña', email: 'erick@frasa.mx' },
{ id: 20400, nombre: 'Cristina Barrera', email: 'cristina@frasa.mx' },
]
const sesionFake = {
token: 'jwt-de-prueba',
usuario: {
@@ -78,64 +73,29 @@ describe('ConfiguracionCuenta con sesión', () => {
localStorage.setItem('frasa_auth', JSON.stringify(sesionFake))
})
it('muestra el perfil, el dropdown de usuarios Monday y guarda el vínculo con PATCH', async () => {
fetchMock.mockImplementation((input, init) => {
const url = String(input)
if (url.endsWith('/auth/me')) return Promise.resolve(respuestaOk(sesionFake.usuario))
if (url.endsWith('/monday/usuarios')) return Promise.resolve(respuestaOk(USUARIOS_MONDAY))
if (url.endsWith('/usuarios/me/monday') && init?.method === 'PATCH') {
return Promise.resolve(respuestaOk({ ...sesionFake.usuario, mondayUserId: 13676 }))
}
return Promise.reject(new Error(`fetch inesperado: ${url}`))
})
render(<ConfiguracionCuenta />)
// Perfil y dropdown cargados
expect(await screen.findByText('ERICK ISRAEL PEÑA RAMOS')).toBeTruthy()
expect(screen.getByText('Rol: Vendedor')).toBeTruthy()
const dropdown = await screen.findByLabelText('Tu usuario de Monday')
expect(screen.getByText('sin vincular')).toBeTruthy()
fireEvent.change(dropdown, { target: { value: '13676' } })
fireEvent.click(screen.getByRole('button', { name: 'Guardar' }))
await waitFor(() => {
const llamadaPatch = fetchMock.mock.calls.find(
([url, init]) => String(url).endsWith('/usuarios/me/monday') && init?.method === 'PATCH',
)
expect(llamadaPatch).toBeTruthy()
expect(JSON.parse(String(llamadaPatch?.[1]?.body))).toEqual({ mondayUserId: 13676 })
})
expect((await screen.findByRole('status')).textContent).toContain('Vínculo con Monday guardado')
// El PATCH fue con Bearer
const [, initPatch] = fetchMock.mock.calls.find(
([url, init]) => String(url).endsWith('/usuarios/me/monday') && init?.method === 'PATCH',
) as [string, RequestInit]
expect((initPatch.headers as Record<string, string>)['Authorization']).toBe('Bearer jwt-de-prueba')
})
it('si falla /monday/usuarios muestra error reintentable y el reintento carga la lista', async () => {
let intentos = 0
it('muestra el perfil (nombre y rol) y el botón de cerrar sesión', async () => {
fetchMock.mockImplementation((input) => {
const url = String(input)
if (url.endsWith('/auth/me')) return Promise.resolve(respuestaOk(sesionFake.usuario))
if (url.endsWith('/monday/usuarios')) {
intentos += 1
return intentos === 1
? Promise.resolve(new Response('{}', { status: 500 }))
: Promise.resolve(respuestaOk(USUARIOS_MONDAY))
}
return Promise.reject(new Error(`fetch inesperado: ${url}`))
})
render(<ConfiguracionCuenta />)
expect(
await screen.findByText('No se pudo cargar la lista de usuarios de Monday.'),
).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'Reintentar' }))
expect(await screen.findByLabelText('Tu usuario de Monday')).toBeTruthy()
expect(intentos).toBe(2)
expect(await screen.findByText('ERICK ISRAEL PEÑA RAMOS')).toBeTruthy()
expect(screen.getByText('Rol: Vendedor')).toBeTruthy()
expect(screen.getByRole('button', { name: 'Cerrar sesión' })).toBeTruthy()
})
it('si falla la carga del perfil la sección sigue usable (logout disponible)', async () => {
fetchMock.mockImplementation((input) => {
const url = String(input)
if (url.endsWith('/auth/me')) return Promise.resolve(new Response('{}', { status: 500 }))
return Promise.reject(new Error(`fetch inesperado: ${url}`))
})
render(<ConfiguracionCuenta />)
expect(await screen.findByRole('button', { name: 'Cerrar sesión' })).toBeTruthy()
})
})

View File

@@ -15,8 +15,8 @@ function formatearRol(rol: string): string {
}
/**
* Sección "Configuración de cuenta": login inline y, con sesión, el vínculo
* con el usuario de Monday (alimenta round-robin, mis-leads y alertas).
* Sección "Configuración de cuenta": login inline y, con sesión, el perfil del
* usuario.
*/
export function ConfiguracionCuenta() {
const [conSesion, setConSesion] = useState(() => getSesion() !== null)

View File

@@ -10,7 +10,7 @@ interface Props {
}
/**
* Vista interna "Contactos": lista del tablero Contactos de Monday (lo más
* Vista interna "Contactos": lista de contactos locales (lo más
* reciente primero). Vive dentro de la landing (no hay router); se abre desde
* la tarjeta Contactos del grid. Click en un contacto → modal con toda su
* información, propiedad, quién lo atiende e historial de atención.

View File

@@ -31,7 +31,7 @@ beforeEach(() => {
// Mockear las llamadas iniciales que los tabs pueden hacer
fetchMock.mockImplementation((input) => {
const url = String(input)
if (url.endsWith('/sistema/estado')) return Promise.resolve(respuestaOk({ mondayOk: true, pendientesSync: 0 }))
if (url.endsWith('/sistema/estado')) return Promise.resolve(respuestaOk({ mondayOk: false, pendientesSync: 0 }))
if (url.endsWith('/config/sistema')) return Promise.resolve(respuestaOk({ modo_local_forzado: 'false' }))
if (url.endsWith('/config/roles')) return Promise.resolve(respuestaOk([]))
if (url.endsWith('/config/usuarios')) return Promise.resolve(respuestaOk([]))