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:
@@ -20,7 +20,7 @@ interface Props {
|
||||
onPasarAAltaCliente: (lead: LeadResumen) => void
|
||||
/** Prefill desde Mis oportunidades (Mi día): valores iniciales del formulario */
|
||||
valoresIniciales?: Partial<typeof FORM_INICIAL>
|
||||
/** Oportunidad de Monday a ACTUALIZAR al enviar (sin duplicar el item) */
|
||||
/** Oportunidad a ACTUALIZAR al enviar (sin duplicar el registro) */
|
||||
oportunidadId?: string
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ const FORM_INICIAL = {
|
||||
|
||||
/**
|
||||
* Captura ligera del interesado al entrar al embudo (POST /leads): crea el
|
||||
* lead en Monday y su oportunidad en etapa Afluencia. El alta completa de
|
||||
* lead y su oportunidad en etapa Afluencia. El alta completa de
|
||||
* cliente sigue siendo el wizard del picker.
|
||||
*/
|
||||
export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valoresIniciales, oportunidadId }: Props) {
|
||||
@@ -90,7 +90,7 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
|
||||
MODELOS_POR_PROYECTO[form.desarrollo as keyof typeof MODELOS_POR_PROYECTO] ?? []
|
||||
|
||||
/**
|
||||
* Jala los datos de un lead u oportunidad ya capturados en Monday:
|
||||
* Jala los datos de un lead u oportunidad ya capturados:
|
||||
* nombre/contacto siempre; origen, desarrollo y modelo solo si el valor del
|
||||
* lead existe entre las opciones del formulario.
|
||||
*/
|
||||
@@ -267,13 +267,13 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
|
||||
className="mb-6 rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 text-xs font-medium text-blue-800"
|
||||
>
|
||||
Estás completando la afluencia de {form.nombre || 'esta oportunidad'} — al enviar se actualiza su
|
||||
oportunidad en Monday (no se duplica).
|
||||
oportunidad ya capturada (no se duplica).
|
||||
</p>
|
||||
) : (
|
||||
<div className="mb-6 border-b border-[#E5E7EB] pb-6">
|
||||
<BusquedaLead onSeleccion={prellenarDesdeLead} />
|
||||
<p className="mt-2 text-xs text-[#64748B] dark:text-[#94A3B8]">
|
||||
Si el interesado ya está en Monday (lead u oportunidad), sus datos se prellenan solos.
|
||||
Si el interesado ya está capturado (lead u oportunidad), sus datos se prellenan solos.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -67,6 +67,6 @@ describe('BusquedaLead', () => {
|
||||
|
||||
fetchMock.mockRejectedValueOnce(new Error('red caída'))
|
||||
fireEvent.change(screen.getByLabelText(/Buscar lead/), { target: { value: 'otro' } })
|
||||
expect((await screen.findByRole('alert')).textContent).toContain('No se pudo buscar en Monday')
|
||||
expect((await screen.findByRole('alert')).textContent).toContain('No se pudo buscar')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ interface Props {
|
||||
}
|
||||
|
||||
/**
|
||||
* Buscador con debounce de leads/afluencias de Monday (GET /leads/buscar,
|
||||
* Buscador con debounce de leads/afluencias (GET /leads/buscar,
|
||||
* búsqueda dual: tablero Leads histórico + Oportunidades). No llama al API
|
||||
* con menos de 2 caracteres; al elegir un resultado avisa con onSeleccion.
|
||||
*/
|
||||
@@ -35,7 +35,7 @@ export default function BusquedaLead({ onSeleccion }: Props) {
|
||||
})
|
||||
.catch(() => {
|
||||
setResultados(null)
|
||||
setError('No se pudo buscar en Monday. Intenta de nuevo.')
|
||||
setError('No se pudo buscar. Intenta de nuevo.')
|
||||
})
|
||||
.finally(() => setBuscando(false))
|
||||
}, DEBOUNCE_MS)
|
||||
|
||||
@@ -94,7 +94,7 @@ export const SLOTS_DOCUMENTOS: { slot: SlotDocumento; etiqueta: string }[] = [
|
||||
|
||||
export const TAMANO_MAX_DOCUMENTO = 10 * 1024 * 1024 // 10 MB
|
||||
|
||||
// Catálogos de Monday (labels exactos). Origen: vocabulario FRASA sembrado en
|
||||
// Catálogos (labels exactos). Origen: vocabulario FRASA sembrado en
|
||||
// los tableros Leads y Oportunidades el 2026-09-04 (reemplaza al set anterior)
|
||||
export const ORIGENES_LEAD = ['PASE', 'PROSPECCION ASESOR', 'ACTIVACION', 'FACEBOOK FRASA', 'REFERIDO', 'BROCKER', 'OTRO'] as const
|
||||
export const DESARROLLOS_LEAD = ['ATELIER', 'DIAGONAL', 'ESTANZA', 'SENDA', 'SOLANA'] as const
|
||||
|
||||
@@ -25,7 +25,7 @@ const base: PrefillApartado = {
|
||||
cotizacion: { tipoPropuesta: 'INFONAVIT', acabados: true, precioFinal: 2646000, enganche: 132300 },
|
||||
}
|
||||
|
||||
describe('apartadoAPrellenado (alta desde apartado de Monday)', () => {
|
||||
describe('apartadoAPrellenado (alta desde apartado)', () => {
|
||||
it('prellena datos generales (contacto manda) y vivienda/crédito (apartado > oportunidad > cotización)', () => {
|
||||
const p = apartadoAPrellenado(base)
|
||||
expect(p.datosGenerales).toMatchObject({
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { DatosGenerales, EstadoCivil, TipoCredito, ViviendaCredito } from '
|
||||
import { PROYECTOS } from '../data/constantes'
|
||||
|
||||
/**
|
||||
* Transformación pura: todo lo recolectado de un apartado de Monday (ficha del
|
||||
* Transformación pura: todo lo recolectado de un apartado (ficha del
|
||||
* apartado + contacto + oportunidad + cotización vinculada) → campos del
|
||||
* wizard de alta. Prellena solo lo que exista (nunca inventa datos).
|
||||
*/
|
||||
@@ -42,7 +42,7 @@ export interface PrellenadoDesdeApartado {
|
||||
leadMondayId: string | null
|
||||
}
|
||||
|
||||
// Label de Monday (Contacto/Oportunidad) → enum del wizard
|
||||
// Label de etapa (Contacto/Oportunidad) → enum del wizard
|
||||
const ESTADO_CIVIL_ENUM: Record<string, EstadoCivil> = {
|
||||
'SOLTERO(A)': 'SOLTERO',
|
||||
SOLTERO: 'SOLTERO',
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { DatosGenerales } from '../types'
|
||||
import type { LeadResumen } from '../services/api'
|
||||
|
||||
/**
|
||||
* Transformación pura lead de Monday → campos del wizard de alta.
|
||||
* Transformación pura lead → campos del wizard de alta.
|
||||
* Prellena solo lo que el lead tenga (nombre en mayúsculas, como el formato).
|
||||
*/
|
||||
export function leadAPrellenado(
|
||||
|
||||
@@ -113,7 +113,7 @@ describe('AltaClientePage — guardado progresivo (etapa por etapa)', () => {
|
||||
expect(await screen.findByLabelText(/Tipo de empleo/)).toBeTruthy()
|
||||
await waitFor(() => {
|
||||
const avisos = screen.getAllByRole('status').map((el) => el.textContent ?? '')
|
||||
expect(avisos.some((a) => a.includes('El avance quedó guardado local'))).toBe(true)
|
||||
expect(avisos.some((a) => a.includes('El avance quedó guardado en este dispositivo'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('AltaClientePage — pantalla de entrada', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('AltaClientePage — alta desde lead de Monday', () => {
|
||||
describe('AltaClientePage — alta desde lead capturado', () => {
|
||||
it('al elegir un lead el wizard arranca con datosGenerales prellenados', async () => {
|
||||
render(<AltaClientePage />)
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ export default function AltaClientePage() {
|
||||
setPantalla('wizard')
|
||||
}
|
||||
|
||||
// Alta nueva prellenada desde un lead de Monday (nombre, email, celular)
|
||||
// Alta nueva prellenada desde un lead ya capturado (nombre, email, celular)
|
||||
const empezarDesdeLead = (lead: LeadResumen) => {
|
||||
reiniciar()
|
||||
setBorradorId(null)
|
||||
@@ -282,7 +282,7 @@ export default function AltaClientePage() {
|
||||
irAPaso(destino)
|
||||
}
|
||||
|
||||
// ---------- Guardado progresivo (borrador local + Contacto en Monday) ----------
|
||||
// ---------- Guardado progresivo (borrador local + alta en progreso) ----------
|
||||
// Guard in-flight: con clicks rápidos se omite el sync en curso; el siguiente
|
||||
// avance manda el estado completo (el PATCH es idempotente).
|
||||
const syncEnCurso = useRef(false)
|
||||
@@ -304,13 +304,13 @@ export default function AltaClientePage() {
|
||||
setAvisoProgreso('')
|
||||
} catch (err) {
|
||||
console.warn('[alta] no se pudo sincronizar el avance con el servidor', err)
|
||||
setAvisoProgreso('El avance quedó guardado local; no se pudo subir a Monday')
|
||||
setAvisoProgreso('El avance quedó guardado en este dispositivo; no se pudo subir al servidor')
|
||||
} finally {
|
||||
syncEnCurso.current = false
|
||||
}
|
||||
}
|
||||
|
||||
/** Al avanzar de paso: guarda el borrador en silencio y sube el avance a Monday */
|
||||
/** Al avanzar de paso: guarda el borrador en silencio y sube el avance al servidor */
|
||||
const guardarAvance = (datosNuevos: typeof datos, pasoDestino: number) => {
|
||||
guardarBorrador({ ...(borradorId ? { id: borradorId } : {}), paso: pasoDestino, datos: datosNuevos })
|
||||
.then((b) => {
|
||||
@@ -519,7 +519,7 @@ export default function AltaClientePage() {
|
||||
|
||||
<section aria-labelledby="titulo-lead" className="mt-12 border-t border-[#E5E7EB] dark:border-[#1E293B] pt-8">
|
||||
<h3 id="titulo-lead" className="text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
|
||||
Desde un lead de Monday
|
||||
Desde un lead ya capturado
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-[#64748B] dark:text-[#94A3B8]">
|
||||
Empieza un alta nueva con el nombre, email y celular del lead ya capturados.
|
||||
@@ -646,7 +646,7 @@ export default function AltaClientePage() {
|
||||
)}
|
||||
{datos.apartadoMondayId && (
|
||||
<p role="note" className="mb-6 rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 text-xs font-medium text-blue-800">
|
||||
Alta ligada al apartado del cliente en Monday — al completarla se actualiza ese item (no se duplica).
|
||||
Alta ligada al apartado del cliente — al completarla se actualiza ese apartado (no se duplica).
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ async function enviarAlta(ruta: string, payload: AltaCliente): Promise<Resultado
|
||||
|
||||
/**
|
||||
* Registra el alta en el API Gateway (POST /clientes). El backend persiste en
|
||||
* SQLite y replica en Monday.
|
||||
* SQLite.
|
||||
*/
|
||||
export async function crearAltaCliente(payload: AltaCliente): Promise<ResultadoAlta> {
|
||||
return enviarAlta('/clientes', payload)
|
||||
@@ -85,7 +85,7 @@ export interface ProgresoIniciado {
|
||||
mondayContactoId: string | null
|
||||
}
|
||||
|
||||
/** POST /clientes/progreso/iniciar — crea el alta en progreso (Contacto PROSPECTO en Monday) */
|
||||
/** POST /clientes/progreso/iniciar — crea el alta en progreso (Contacto PROSPECTO) */
|
||||
export async function iniciarProgreso(payload: {
|
||||
datosGenerales: unknown
|
||||
leadMondayId?: string
|
||||
@@ -216,7 +216,7 @@ export async function obtenerCotizacion(id: string): Promise<import('../lib/coti
|
||||
return (await res.json()) as import('../lib/cotizacionAlta').CotizacionParaAlta
|
||||
}
|
||||
|
||||
/** GET /altas/prefill-apartado/:itemId — todo lo recolectado de un apartado de Monday (?apartado=<id>) */
|
||||
/** GET /altas/prefill-apartado/:itemId — todo lo recolectado de un apartado (?apartado=<id>) */
|
||||
export async function obtenerPrefillApartado(itemId: string): Promise<import('../lib/apartadoAlta').PrefillApartado> {
|
||||
const res = await fetch(`${API_URL}/altas/prefill-apartado/${itemId}`, conAuth())
|
||||
manejar401(res)
|
||||
@@ -224,7 +224,7 @@ export async function obtenerPrefillApartado(itemId: string): Promise<import('..
|
||||
return (await res.json()) as import('../lib/apartadoAlta').PrefillApartado
|
||||
}
|
||||
|
||||
// ---------- Pre-llenado desde lead de Monday (auth) ----------
|
||||
// ---------- Pre-llenado desde lead (auth) ----------
|
||||
|
||||
export interface LeadResumen {
|
||||
id: string
|
||||
@@ -279,7 +279,7 @@ export async function crearAfluencia(payload: {
|
||||
origen?: string
|
||||
desarrollo?: string
|
||||
notas?: string
|
||||
/** Oportunidad de Monday a actualizar (botón "Afluencia" de Mi día) */
|
||||
/** Oportunidad a actualizar (botón "Afluencia" de Mi día) */
|
||||
oportunidadId?: string
|
||||
modelo?: string
|
||||
numDependientes?: string
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { AltaCliente } from '../types'
|
||||
|
||||
/**
|
||||
* OBSOLETO: la réplica en Monday (tableros Contactos y Apartado) ya la hace
|
||||
* el API Gateway en POST /api/clientes (ver services/api.ts y
|
||||
* api-gateway/src/controllers/clientes.controller.ts). Se conserva el
|
||||
* archivo solo como referencia; no llamar desde el flujo del wizard.
|
||||
*/
|
||||
export async function enviarAMonday(payload: AltaCliente): Promise<void> {
|
||||
console.warn('[monday] enviarAMonday está obsoleto: el backend ya replica en Monday', {
|
||||
nombre: payload.datosGenerales.nombreCompleto,
|
||||
proyecto: payload.viviendaCredito.proyecto,
|
||||
ubicacion: payload.viviendaCredito.ubicacion,
|
||||
})
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { conAuth, getToken } from './auth'
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || '/api'
|
||||
|
||||
export interface EstadoSistema {
|
||||
mondayOk: boolean
|
||||
pendientesSync: number
|
||||
}
|
||||
|
||||
export async function fetchEstadoSistema(): Promise<EstadoSistema | null> {
|
||||
if (!getToken()) return null
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/sistema/estado`, conAuth())
|
||||
if (!res.ok) return null
|
||||
return (await res.json()) as EstadoSistema
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -147,10 +147,10 @@ export interface DatosFirma {
|
||||
}
|
||||
|
||||
export interface AltaCliente {
|
||||
// ID del item del tablero Leads de Monday cuando el alta se prellenó desde
|
||||
// ID del lead cuando el alta se prellenó desde
|
||||
// un lead (null = captura manual). Passthrough hasta payloadJson del API.
|
||||
leadMondayId: string | null
|
||||
// ID del item del tablero Apartado de Monday cuando el alta arrancó desde un
|
||||
// ID del apartado cuando el alta arrancó desde un
|
||||
// apartado existente (botón "Alta Enkontrol" de Mis apartados): el
|
||||
// API lo LIGA y enriquece en vez de crear un apartado duplicado.
|
||||
apartadoMondayId: string | null
|
||||
|
||||
Reference in New Issue
Block a user