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:
@@ -12,6 +12,7 @@ import { ComparadorPropuestas } from '../components/ComparadorPropuestas'
|
||||
import { ComparadorUnidades } from '../components/ComparadorUnidades'
|
||||
import { MisCotizaciones } from '../components/MisCotizaciones'
|
||||
import { ModalCliente } from '../components/ModalCliente'
|
||||
import { BuscadorCliente } from '../components/BuscadorCliente'
|
||||
import { generarPdf } from '../lib/pdf'
|
||||
import { formatoMoneda, formatoFechaLarga } from '../lib/formatters'
|
||||
import { calcularVigencia } from '../lib/vigencia'
|
||||
@@ -60,7 +61,9 @@ export default function CotizadorPage() {
|
||||
setApartado,
|
||||
apartadoEfectivo,
|
||||
errorApartado,
|
||||
setDescuento,
|
||||
enganchePct,
|
||||
setEnganchePct,
|
||||
errorEnganche,
|
||||
propuestaIndex,
|
||||
setPropuestaIndex,
|
||||
precioLista,
|
||||
@@ -154,7 +157,7 @@ export default function CotizadorPage() {
|
||||
|
||||
// La propuesta elegida (para guardar/compartir); acotada por si cambia el desarrollo
|
||||
const propuestaActiva = propuestas[Math.min(propuestaIndex, Math.max(propuestas.length - 1, 0))]
|
||||
const puedeGenerarPdf = Boolean(unidad) && !errorApartado && !guardando
|
||||
const puedeGenerarPdf = Boolean(unidad) && !errorApartado && !errorEnganche && !guardando
|
||||
|
||||
/** Persiste la cotización en el API; null si no hay conexión (el PDF sale igual, sin folio/QR) */
|
||||
const guardarEnApi = async (cliente: Cliente): Promise<CotizacionGuardada | null> => {
|
||||
@@ -169,6 +172,7 @@ export default function CotizadorPage() {
|
||||
acabados,
|
||||
apartado: apartadoEfectivo,
|
||||
descuento: propuestaActiva.descuento,
|
||||
...(enganchePct !== null && errorEnganche === null ? { enganchePct: enganchePct / 100 } : {}),
|
||||
...(oportunidadId !== null ? { oportunidadId } : {}),
|
||||
})
|
||||
const conFolio = { ...guardada, folio: guardada.folio ?? folioCotizacion(guardada.id) }
|
||||
@@ -419,6 +423,54 @@ export default function CotizadorPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="enganche" className={etiquetaCampo}>
|
||||
Enganche (%) — opcional
|
||||
</label>
|
||||
<input
|
||||
id="enganche"
|
||||
type="number"
|
||||
min={2.5}
|
||||
max={100}
|
||||
step={0.5}
|
||||
placeholder="Según propuesta"
|
||||
value={enganchePct ?? ''}
|
||||
onChange={(e) => {
|
||||
const crudo = e.target.value
|
||||
if (crudo === '') {
|
||||
setEnganchePct(null)
|
||||
return
|
||||
}
|
||||
const v = Number(crudo)
|
||||
setEnganchePct(Number.isFinite(v) ? v : null)
|
||||
}}
|
||||
className={errorEnganche ? campoInputError : campoInput}
|
||||
/>
|
||||
{errorEnganche ? (
|
||||
<p className="mt-1 text-xs text-red-700">{errorEnganche}</p>
|
||||
) : (
|
||||
<p className="mt-1 text-xs text-[#64748B]">
|
||||
Aplica a crédito bancario e Infonavit/Cofinavit; mínimo 2.5%
|
||||
{unidad ? ` (${formatoMoneda((precioLista * Math.max(enganchePct ?? 2.5, 2.5)) / 100)})` : ''}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BuscadorCliente
|
||||
onSeleccion={(lead) => {
|
||||
setValue('nombre', lead.nombre, { shouldValidate: true })
|
||||
if (lead.telefono) setValue('telefono', lead.telefono, { shouldValidate: true })
|
||||
if (lead.email) setValue('email', lead.email, { shouldValidate: true })
|
||||
if (lead.fuente === 'oportunidad') setOportunidadId(lead.id)
|
||||
if (lead.desarrollo && LISTA_DESARROLLOS.includes(lead.desarrollo)) setDesarrollo(lead.desarrollo)
|
||||
setAviso(
|
||||
`Datos de ${lead.nombre} cargados${
|
||||
lead.fuente === 'oportunidad' ? ' — la cotización quedará vinculada a su oportunidad' : ''
|
||||
}.`,
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label htmlFor="nombre" className={etiquetaCampo}>
|
||||
Nombre del cliente *
|
||||
@@ -502,7 +554,6 @@ export default function CotizadorPage() {
|
||||
numero={i + 1}
|
||||
titulo={p.titulo}
|
||||
propuesta={p}
|
||||
onDescuentoChange={(v) => setDescuento(params.propuestas[i].claveDescuento, v)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user