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

@@ -1,6 +1,7 @@
import { useState, type FormEvent } from 'react'
import { z } from 'zod'
import { crearAfluencia, type LeadResumen } from '../services/api'
import BusquedaLead from './BusquedaLead'
import {
DESARROLLOS_LEAD,
ESTADOS_CIVILES,
@@ -78,6 +79,36 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Pro
const modelosDelDesarrollo =
MODELOS_POR_PROYECTO[form.desarrollo as keyof typeof MODELOS_POR_PROYECTO] ?? []
/**
* Jala los datos de un lead u oportunidad ya capturados en Monday:
* nombre/contacto siempre; origen, desarrollo y modelo solo si el valor del
* lead existe entre las opciones del formulario.
*/
const prellenarDesdeLead = (lead: LeadResumen) => {
setForm((prev) => {
const desarrollo =
lead.desarrollo !== undefined && (DESARROLLOS_LEAD as readonly string[]).includes(lead.desarrollo)
? lead.desarrollo
: prev.desarrollo
const modelos = MODELOS_POR_PROYECTO[desarrollo as keyof typeof MODELOS_POR_PROYECTO] ?? []
const modelo =
lead.modeloInteres !== undefined && (modelos as readonly string[]).includes(lead.modeloInteres)
? lead.modeloInteres
: ''
return {
...prev,
nombre: lead.nombre,
telefono: lead.telefono,
email: lead.email,
origen: (ORIGENES_LEAD as readonly string[]).includes(lead.origen) ? lead.origen : prev.origen,
desarrollo,
modelo,
}
})
setErrores({})
setAviso('')
}
const validar = (): boolean => {
const nuevos: Errores = {}
if (!form.nombre.trim()) nuevos.nombre = 'El nombre es obligatorio'
@@ -180,6 +211,12 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Pro
return (
<form onSubmit={(e) => void enviar(e)} noValidate>
<div className="mb-6 border-b border-[#E5E7EB] pb-6">
<BusquedaLead onSeleccion={prellenarDesdeLead} />
<p className="mt-2 text-xs text-[#64748B]">
Si el interesado ya está en Monday (lead u oportunidad), sus datos se prellenan solos.
</p>
</div>
<div className="grid gap-6 sm:grid-cols-2">
<label htmlFor="afluencia-nombre" className="block sm:col-span-2">
<span className={labelClase}>