feat(cotizador): historial, compartir WhatsApp, folio+QR, comparador de unidades, alertas de inventario

- Mis cotizaciones (guardar con cliente, retomar, nueva) asociadas al vendedor del token
- Compartir: resumen copiable + deep link wa.me; PDF con folio, vigencia y QR
- GET /api/cotizaciones/:id/publico (público, datos mínimos sin PII del cliente)
- Comparador lado a lado de 2 unidades (mismo o distinto desarrollo)
- POST /api/cotizaciones/alertas-inventario: avisa al vendedor si una unidad
  cotizada cambia de estatus (dedup; wire con syncs documentado)
- Fix: labels de propuesta mapeados a los reales del board Oportunidades
- Tests: api 201, cotizador 82
This commit is contained in:
2026-08-28 06:58:10 +00:00
parent bc0f9da8fe
commit 4f31d1c171
24 changed files with 1938 additions and 20 deletions

View File

@@ -28,9 +28,27 @@ interface UseCotizacionResult {
errorApartado: string | null
descuentos: DescuentosPropuestas
setDescuento: (clave: keyof DescuentosPropuestas, valor: number) => void
/** Índice de la propuesta elegida (para guardar/compartir) */
propuestaIndex: number
setPropuestaIndex: (i: number) => void
precioLista: number
numMeses: number | null
propuestas: PropuestaResultado[]
/** Restaura el estado completo desde una cotización guardada (historial) */
restaurar: (estado: EstadoRestaurar) => void
/** Reinicia la cotización (botón "Nueva") */
nueva: () => void
}
export interface EstadoRestaurar {
desarrollo: string
/** Unidad ya resuelta del inventario cargado */
unidad: Unidad
acabados: boolean
apartado: number
/** Descuento (fracción) de la propuesta elegida; se aplica sobre su clave */
descuento?: number
propuestaIndex?: number
}
/** Estado central de la cotización: desarrollo, unidad, acabados, apartado y descuentos */
@@ -40,6 +58,7 @@ export function useCotizacion(): UseCotizacionResult {
const [acabados, setAcabados] = useState(false)
const [apartado, setApartado] = useState<number>(APARTADO_DEFAULT)
const [descuentos, setDescuentos] = useState<DescuentosPropuestas>(descuentosDefault(DESARROLLO_DEFAULT))
const [propuestaIndex, setPropuestaIndex] = useState(0)
const params = obtenerDesarrollo(desarrollo)
@@ -48,12 +67,36 @@ export function useCotizacion(): UseCotizacionResult {
setUnidad(null)
setAcabados(false)
setDescuentos(descuentosDefault(id))
setPropuestaIndex(0)
}
const setDescuento = (clave: keyof DescuentosPropuestas, valor: number) => {
setDescuentos((prev) => ({ ...prev, [clave]: valor }))
}
const restaurar = (estado: EstadoRestaurar) => {
setDesarrolloState(estado.desarrollo)
setUnidad(estado.unidad)
setAcabados(estado.acabados)
setApartado(estado.apartado)
const base = descuentosDefault(estado.desarrollo)
if (estado.descuento !== undefined) {
const idx = estado.propuestaIndex ?? 0
const clave = obtenerDesarrollo(estado.desarrollo).propuestas[idx]?.claveDescuento
if (clave) base[clave] = estado.descuento
}
setDescuentos(base)
setPropuestaIndex(estado.propuestaIndex ?? 0)
}
const nueva = () => {
setUnidad(null)
setAcabados(false)
setApartado(APARTADO_DEFAULT)
setDescuentos(descuentosDefault(desarrollo))
setPropuestaIndex(0)
}
const numMeses = useMemo(
() => (unidad ? resolverNumMensualidades(unidad, params) : null),
[unidad, params],
@@ -87,8 +130,12 @@ export function useCotizacion(): UseCotizacionResult {
errorApartado,
descuentos,
setDescuento,
propuestaIndex,
setPropuestaIndex,
precioLista,
numMeses,
propuestas,
restaurar,
nueva,
}
}