feat: docs en apartados, enganche seleccionable y prefill de clientes

- Mis apartados (Mi día): botón Documentos con precarga del expediente —
  captura del cotizador y captura de pago apartado (obligatorios) más
  documentos generales; los archivos se guardan en el cliente ligado al
  apartado (se crea la ficha mínima si no existe). Endpoints
  GET/POST /apartados/:id/documentos (multipart, 10 MB, pdf/jpg/png)
- Cotizador: se oculta el editor de descuento (usan los % del Excel);
  enganche seleccionable con mínimo 2.5% del valor de la propiedad, solo
  en esquemas con crédito (bancario/Infonavit); mensualidades se
  recalculan con el enganche elegido y nunca quedan en negativo
- Cotizador: buscador de cliente en Monday (lead u oportunidad) que
  prellena nombre/contacto/desarrollo y vincula la cotización a la
  oportunidad al guardar
- Alta Afluencia: buscador que jala nombre, contacto, origen, desarrollo
  y modelo desde leads y oportunidades existentes
- buscarLeads devuelve desarrollo y modelo de interés
- Landing: el módulo Clientes vuelve a llamarse Cobranza
- Tests: 456 API + 94 cotizador + 14 mi-dia + 116 alta-clientes, verde
This commit is contained in:
2026-09-04 22:32:40 +00:00
parent 1963c06d93
commit cbd50e2066
19 changed files with 700 additions and 44 deletions

View File

@@ -7,6 +7,7 @@ import {
obtenerPrecioLista,
resolverNumMensualidades,
validarApartado,
validarEnganchePct,
type PropuestaResultado,
} from '../lib/calculations'
import { DESARROLLO_DEFAULT, descuentosDefault, obtenerDesarrollo } from '../data/desarrollos'
@@ -26,6 +27,11 @@ interface UseCotizacionResult {
apartadoEfectivo: number
/** Mensaje de error de la captura del apartado; null si es válida */
errorApartado: string | null
/** Enganche capturado en PORCENTAJE (10 = 10%); null = % del Excel de cada propuesta */
enganchePct: number | null
setEnganchePct: (v: number | null) => void
/** Mensaje de error del enganche capturado; null si es válido */
errorEnganche: string | null
descuentos: DescuentosPropuestas
setDescuento: (clave: keyof DescuentosPropuestas, valor: number) => void
/** Índice de la propuesta elegida (para guardar/compartir) */
@@ -57,6 +63,7 @@ export function useCotizacion(): UseCotizacionResult {
const [unidad, setUnidad] = useState<Unidad | null>(null)
const [acabados, setAcabados] = useState(false)
const [apartado, setApartado] = useState<number>(APARTADO_DEFAULT)
const [enganchePct, setEnganchePct] = useState<number | null>(null)
const [descuentos, setDescuentos] = useState<DescuentosPropuestas>(descuentosDefault(DESARROLLO_DEFAULT))
const [propuestaIndex, setPropuestaIndex] = useState(0)
@@ -93,6 +100,7 @@ export function useCotizacion(): UseCotizacionResult {
setUnidad(null)
setAcabados(false)
setApartado(APARTADO_DEFAULT)
setEnganchePct(null)
setDescuentos(descuentosDefault(desarrollo))
setPropuestaIndex(0)
}
@@ -111,10 +119,14 @@ export function useCotizacion(): UseCotizacionResult {
const errorApartado = validarApartado(apartado)
const apartadoEfectivo = Math.max(apartado, APARTADO_MINIMO)
// Enganche personalizado: null = default del Excel; inválido no entra al cálculo
const errorEnganche = validarEnganchePct(enganchePct)
const engancheEfectivo = errorEnganche === null && enganchePct !== null ? enganchePct / 100 : null
const propuestas = useMemo(() => {
if (!unidad) return []
return calcularPropuestas({ unidad, acabados, apartado: apartadoEfectivo, descuentos, desarrollo })
}, [unidad, acabados, apartadoEfectivo, descuentos, desarrollo])
return calcularPropuestas({ unidad, acabados, apartado: apartadoEfectivo, descuentos, desarrollo, enganchePct: engancheEfectivo })
}, [unidad, acabados, apartadoEfectivo, descuentos, desarrollo, engancheEfectivo])
return {
desarrollo,
@@ -128,6 +140,9 @@ export function useCotizacion(): UseCotizacionResult {
setApartado,
apartadoEfectivo,
errorApartado,
enganchePct,
setEnganchePct,
errorEnganche,
descuentos,
setDescuento,
propuestaIndex,