fix(auditoria completa): bloqueos de Mi Dia, cobranza (saldos y fechas), captura (departamento y rol), contactos (privacidad), cotizador (QR) y validaciones de API
This commit is contained in:
@@ -18,9 +18,17 @@ export function formatoFechaLarga(fecha: Date): string {
|
||||
return fecha.toLocaleDateString('es-MX', { day: 'numeric', month: 'long', year: 'numeric' })
|
||||
}
|
||||
|
||||
/** ISO 'YYYY-MM-DD' a fecha legible corta: "08/10/2027" */
|
||||
export function formatoFechaCorta(iso: string): string {
|
||||
return new Date(`${iso}T00:00:00`).toLocaleDateString('es-MX', {
|
||||
/**
|
||||
* Fecha ISO ('YYYY-MM-DD' o ISO completo con hora, como lo manda el API) a
|
||||
* fecha legible corta: "08/10/2027". Antes se concatenaba 'T00:00:00' a la
|
||||
* cadena y con las fechas con hora salía "Invalid Date" en los PDFs.
|
||||
*/
|
||||
export function formatoFechaCorta(iso: string | null | undefined): string {
|
||||
if (!iso) return '—'
|
||||
const soloFecha = String(iso).slice(0, 10)
|
||||
const fecha = new Date(`${soloFecha}T00:00:00`)
|
||||
if (Number.isNaN(fecha.getTime())) return '—'
|
||||
return fecha.toLocaleDateString('es-MX', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('getUnidades', () => {
|
||||
|
||||
const resultado = await getUnidades()
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith('http://192.168.10.123:3000/api/unidades', {})
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/unidades', {})
|
||||
expect(resultado.usandoSeed).toBe(false)
|
||||
expect(resultado.unidades).toEqual([unidadApi])
|
||||
})
|
||||
|
||||
@@ -91,9 +91,16 @@ export interface GuardarCotizacionPayload {
|
||||
oportunidadId?: string
|
||||
}
|
||||
|
||||
/** URL pública de una cotización (destino del QR del PDF) */
|
||||
/**
|
||||
* URL pública de una cotización (destino del QR del PDF). Debe ser ABSOLUTA:
|
||||
* un QR con ruta relativa ("/api/...") no es navegable al escanearlo desde el
|
||||
* teléfono del cliente. Se arma con el origen actual cuando API_URL es relativa.
|
||||
*/
|
||||
export function urlPublicaCotizacion(id: string): string {
|
||||
return `${API_URL}/cotizaciones/${id}/publico`
|
||||
const base = API_URL.startsWith('http')
|
||||
? API_URL
|
||||
: `${typeof window !== 'undefined' ? window.location.origin : ''}${API_URL}`
|
||||
return `${base}/cotizaciones/${id}/publico`
|
||||
}
|
||||
|
||||
/** POST /cotizaciones — persiste y liga la oportunidad del cliente */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Fragment, useMemo, useState, useCallback } from 'react'
|
||||
import { Fragment, useEffect, useMemo, useState, useCallback } from 'react'
|
||||
import type { CampoDef, DatosCaptura } from '../services/captura'
|
||||
|
||||
// ─── Tipos ──────────────────────────────────────────────────────────
|
||||
@@ -185,6 +185,14 @@ export function TablaCaptura({
|
||||
const [dirty, setDirty] = useState<DirtyMap>({})
|
||||
const [pagina, setPagina] = useState(0)
|
||||
|
||||
// Al cambiar de departamento o de desarrollo los campos son otros: los cambios
|
||||
// pendientes de la vista anterior se descartan (antes se guardaban en el
|
||||
// departamento equivocado) y la paginación vuelve al inicio.
|
||||
useEffect(() => {
|
||||
setDirty({})
|
||||
setPagina(0)
|
||||
}, [campos])
|
||||
|
||||
/** Obtener estatus de una unidad (campo estatus en dept VENTAS, siempre disponible) */
|
||||
const estatusUnidad = useCallback(
|
||||
(unidad: string): string => {
|
||||
@@ -252,12 +260,20 @@ export function TablaCaptura({
|
||||
[datos],
|
||||
)
|
||||
|
||||
/** Enviar cambios al padre */
|
||||
/**
|
||||
* Enviar cambios al padre. NO se limpian aquí: el padre recarga los datos al
|
||||
* guardar con éxito y el efecto de abajo limpia los pendientes. Si el guardado
|
||||
* falla, lo capturado sigue en pantalla para reintentar.
|
||||
*/
|
||||
const handleSubmit = useCallback(() => {
|
||||
onGuardar(dirty)
|
||||
setDirty({})
|
||||
}, [dirty, onGuardar])
|
||||
|
||||
// Los datos guardados llegaron (recarga tras guardar): ya no hay pendientes
|
||||
useEffect(() => {
|
||||
setDirty({})
|
||||
}, [datos])
|
||||
|
||||
if (unidades.length === 0) {
|
||||
return (
|
||||
<p className="text-sm text-[#64748B] dark:text-[#94A3B8]">
|
||||
|
||||
@@ -79,7 +79,6 @@ export default function KanbanView<T extends { id: string; nombre: string; estad
|
||||
</div>
|
||||
{item.telefono && <p className="mt-1 text-xs text-[#94A3B8]">{item.telefono}</p>}
|
||||
{item.unidadCodigo && <p className="mt-0.5 text-xs text-[#94A3B8]">{item.unidadCodigo}</p>}
|
||||
{item.pendienteSync && <span className="mt-1 inline-block rounded-full bg-amber-50 px-1.5 py-0.5 text-[10px] text-amber-700 dark:bg-amber-900/30 dark:text-amber-400">⏳ sync</span>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1038,8 +1038,11 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
||||
setAviso(null)
|
||||
try {
|
||||
await cambiarEtapaOportunidad(op.id, 'Cotizador')
|
||||
await cargar() // refleja la etapa nueva sin recargar la página
|
||||
} catch {
|
||||
toast.error('No se pudo actualizar la etapa; el cotizador se abrirá de todos modos.')
|
||||
} finally {
|
||||
setEnviando(false) // sin esto la página quedaba bloqueada hasta recargar
|
||||
}
|
||||
window.open(urlCotizadorParaOportunidad(op), '_blank')
|
||||
}
|
||||
@@ -1128,7 +1131,20 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones
|
||||
const enEscrituracion = resumen
|
||||
? (resumen.todosApartados || resumen.misApartados).filter((a) => {
|
||||
const e = (a.estado ?? '').toUpperCase()
|
||||
return e.includes('FIRMA') || e.includes('VENTA') || e.includes('SEGUIMIENTO') || e.includes('INTEGRACION') || e.includes('X AUTORIZAR') || e.includes('AUTORIZADO') || e.includes('X ESCRITURAR')
|
||||
return (
|
||||
e.includes('FIRMA') ||
|
||||
e.includes('VENTA') ||
|
||||
e.includes('SEGUIMIENTO') ||
|
||||
e.includes('INTEGRACION') ||
|
||||
e.includes('X AUTORIZAR') ||
|
||||
e.includes('AUTORIZADO') ||
|
||||
e.includes('X ESCRITURAR') ||
|
||||
// Estados finales: sin ellos el registro desaparecía de Mi Día al
|
||||
// marcarlo como Escriturado o Titulado
|
||||
e.includes('ESCRITURADO') ||
|
||||
e.includes('TITULADO') ||
|
||||
e.includes('CANCO')
|
||||
)
|
||||
})
|
||||
: []
|
||||
const escrituracionFiltrados = aplicarFiltros(enEscrituracion, filtrosPorTab.escrituracion, {
|
||||
|
||||
Reference in New Issue
Block a user