feat(pendientes): cotizador (mensualidades, bloqueo de unidad, totales, numMensualidades), cobranza (recibos, atrasos, mora configurable, recordatorios), soporte (adjuntos, filtros, estados), clientes maestro (catalogos reales), contactos (ficha y duplicados) y permisos de captura por rol

This commit is contained in:
2026-09-21 08:34:53 +00:00
parent b5a20aed4e
commit 2fc80e7f9e
66 changed files with 3792 additions and 436 deletions

View File

@@ -20,6 +20,7 @@ const respuesta = (data: unknown, status = 200) =>
const almacenamiento = new Map<string, string>()
beforeEach(() => {
almacenamiento.clear()
window.history.replaceState({}, '', '/')
vi.stubGlobal('localStorage', {
getItem: (k: string) => almacenamiento.get(k) ?? null,
setItem: (k: string, v: string) => void almacenamiento.set(k, v),
@@ -88,4 +89,58 @@ describe('CotizadorPage — barra sticky y guardado local', () => {
})
expect(screen.getByText(/cotización guardada localmente con folio CTE-LOCAL-/)).toBeTruthy()
})
it('no deja cotizar una unidad que ya no está disponible: avisa y bloquea las acciones', async () => {
const vendida = { ...unidad, codigo: 'U-900', status: 'VENDIDO' as const }
const cotizacionVendida = {
id: 'cot-vendida',
folio: 'COT-VENDID',
unidadCodigo: vendida.codigo,
desarrollo: 'DIAGONAL',
clienteNombre: 'Cliente Antiguo',
clienteTelefono: null,
clienteEmail: null,
tipoPropuesta: 'BANCARIO',
acabados: false,
apartado: 20000,
descuento: 0,
precioLista: vendida.precioConAcabados,
precioFinal: 0,
enganche: 0,
numMensualidades: 12,
mensualidad: 1000,
pagoFinal: 0,
createdAt: '2026-09-01T00:00:00.000Z',
}
const fetchMock = vi.fn((input: RequestInfo | URL) => {
const url = String(input)
if (url.includes('/unidades')) return Promise.resolve(respuesta([unidad, vendida]))
if (url.includes('/cotizaciones/mias')) return Promise.resolve(respuesta([cotizacionVendida]))
return Promise.reject(new TypeError(`fetch inesperado: ${url}`))
})
vi.stubGlobal('fetch', fetchMock)
// Prellenado desde Mi día con una unidad que ya se vendió
window.history.replaceState({}, '', `/?unidad=${vendida.codigo}`)
render(
<MemoryRouter>
<CotizadorPage />
</MemoryRouter>,
)
// El prellenado NO la preselecciona y lo dice
expect(await screen.findByText(/ya no está disponible \(estatus: VENDIDO\)/)).toBeTruthy()
// Retomar la cotización vieja de esa unidad: se restaura, pero avisa y
// deja Guardar/Compartir/Generar PDF bloqueados
fireEvent.click(await screen.findByRole('button', { name: /Ver historial/ }))
fireEvent.click(await screen.findByRole('button', { name: 'Retomar' }))
const alerta = await screen.findByRole('alert')
expect(alerta.textContent).toContain(`La unidad ${vendida.codigo} no está disponible`)
expect(screen.getByText(/ya no está disponible \(estatus: VENDIDO\): no se puede cotizar de nuevo/)).toBeTruthy()
for (const nombre of ['Generar PDF', 'Guardar', 'Compartir']) {
expect((screen.getByRole('button', { name: nombre }) as HTMLButtonElement).disabled).toBe(true)
}
})
})