feat(mejoras): 25 mejoras UX en todos los módulos + purga Monday

Backend: mi-dia siempre local, vocabulario canónico, AplicacionPago con usuario
Mi día: modal español real, cita desde modal, score falso eliminado
Cotizador: barra sticky, persistencia local con folio
Alta Clientes: redirect eliminado, foto conectada, asesor desde sesión, refs dinámicas
Cobranza: contrato Mora arreglado, link a Plan, abono próximo pago, WhatsApp
Landing: contactos locales, ModalContacto eliminado, ModoTrabajo eliminado
Dashboards: MiTablero unificado, funnel con descartados, drill-down, rango fechas
Chat FRASA: corpus 5 desarrollos, 6 herramientas, historial conversaciones

69 archivos, 7 apps construidas
This commit is contained in:
2026-09-11 06:52:37 +00:00
parent 075d47c304
commit 64b01246d6
69 changed files with 1600 additions and 1133 deletions

View File

@@ -0,0 +1,91 @@
// @vitest-environment jsdom
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import CotizadorPage from './CotizadorPage'
import { INVENTARIO_SEED } from '../data/unidades'
/**
* Smoke del cierre de la cotización:
* - barra sticky con propuesta elegida + Compartir/Guardar/Generar PDF
* - preselección por defecto de la propuesta de menor total
* - sin servidor: la cotización se guarda localmente con folio CTE-LOCAL-*
*/
const unidad = INVENTARIO_SEED.find((u) => u.status === 'DISPONIBLE' && u.desarrollo === 'DIAGONAL')!
const respuesta = (data: unknown, status = 200) =>
new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json' } })
// El entorno de test no trae localStorage: mock en memoria para el fallback local
const almacenamiento = new Map<string, string>()
beforeEach(() => {
almacenamiento.clear()
vi.stubGlobal('localStorage', {
getItem: (k: string) => almacenamiento.get(k) ?? null,
setItem: (k: string, v: string) => void almacenamiento.set(k, v),
removeItem: (k: string) => void almacenamiento.delete(k),
clear: () => almacenamiento.clear(),
})
})
afterEach(() => {
cleanup()
vi.unstubAllGlobals()
})
describe('CotizadorPage — barra sticky y guardado local', () => {
it('preselecciona la propuesta más barata y guarda local con folio CTE-LOCAL cuando el API no responde', async () => {
const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input)
const metodo = init?.method ?? 'GET'
if (url.includes('/unidades')) return Promise.resolve(respuesta([unidad]))
if (url.includes('/cotizaciones/mias')) return Promise.resolve(respuesta([]))
if (url.includes('/cotizaciones') && metodo === 'POST') {
return Promise.reject(new TypeError('servidor caído'))
}
return Promise.reject(new TypeError(`fetch inesperado: ${metodo} ${url}`))
})
vi.stubGlobal('fetch', fetchMock)
render(
<MemoryRouter>
<CotizadorPage />
</MemoryRouter>,
)
// Barra sticky siempre presente, con las tres acciones
expect(await screen.findByRole('button', { name: 'Generar PDF' })).toBeTruthy()
expect(screen.getByRole('button', { name: 'Guardar' })).toBeTruthy()
expect(screen.getByRole('button', { name: 'Compartir' })).toBeTruthy()
expect(screen.getByText(/Selecciona una unidad para elegir propuesta/)).toBeTruthy()
// Elegir una unidad del inventario
const inputUnidad = screen.getByLabelText('Unidad') as HTMLInputElement
fireEvent.focus(inputUnidad)
fireEvent.change(inputUnidad, { target: { value: unidad.codigo } })
fireEvent.mouseDown(screen.getByRole('option', { name: new RegExp(unidad.codigo) }))
// Preselección por defecto: la propuesta de menor total pagado
const selectPropuesta = (await screen.findByLabelText(
'Cambiar propuesta elegida',
)) as HTMLSelectElement
await waitFor(() => expect(selectPropuesta.value).toBe('2')) // Contado es la más barata
expect(screen.getByText(/Propuesta elegida: #3/)).toBeTruthy()
// Guardar con el servidor caído → folio local persistido en el dispositivo
fireEvent.change(screen.getByLabelText(/Nombre del cliente/), {
target: { value: 'Cliente Prueba Local' },
})
fireEvent.click(screen.getByRole('button', { name: 'Guardar' }))
await waitFor(() => {
const locales = JSON.parse(
almacenamiento.get('frasa-cotizador:cotizaciones-locales') ?? '[]',
) as { folio?: string; clienteNombre?: string }[]
expect(locales).toHaveLength(1)
expect(locales[0]?.folio ?? '').toMatch(/^CTE-LOCAL-\d+$/)
expect(locales[0]?.clienteNombre).toBe('Cliente Prueba Local')
})
expect(screen.getByText(/cotización guardada localmente con folio CTE-LOCAL-/)).toBeTruthy()
})
})