feat(cotizador): multi-desarrollo — ATELIER, ESTANZA, SENDA, SOLANA + DIAGONAL
- Motor de cálculo parametrizado por desarrollo (enganches, descuentos, splits, fuente de N, quirk Infonavit-lista); DIAGONAL byte-idéntico - 285 unidades (5 desarrollos) en seeds, API (?desarrollo=) y board Monday - Selector de desarrollo en UI, propuestas por config, PDF multi-desarrollo - Knowledge IA: catálogo por desarrollo + matriz de diferencias (10 docs) - Tests: cotizador 53, api 99 — fixtures de los Excel validados al centavo
This commit is contained in:
@@ -10,6 +10,7 @@ import { ComparadorPropuestas } from '../components/ComparadorPropuestas'
|
||||
import { generarPdf } from '../lib/pdf'
|
||||
import { formatoMoneda, formatoFechaLarga } from '../lib/formatters'
|
||||
import { calcularVigencia } from '../lib/vigencia'
|
||||
import { LISTA_DESARROLLOS, obtenerDesarrollo } from '../data/desarrollos'
|
||||
import type { Cliente } from '../types'
|
||||
|
||||
const esquemaCliente = z.object({
|
||||
@@ -22,13 +23,6 @@ const esquemaCliente = z.object({
|
||||
|
||||
type FormularioCliente = z.infer<typeof esquemaCliente>
|
||||
|
||||
const TITULOS_PROPUESTAS: [string, keyof import('../types').DescuentosPropuestas][] = [
|
||||
['Crédito Bancario', 'creditoBancario'],
|
||||
['Crédito Infonavit-Cofinavit', 'creditoInfonavit'],
|
||||
['Contado', 'contado'],
|
||||
['Semi Contado', 'semiContado'],
|
||||
]
|
||||
|
||||
const campoInputBase =
|
||||
'w-full border bg-white px-3 py-2.5 text-sm transition-colors duration-150 focus:border-neutral-900 focus:outline-none'
|
||||
const campoInput = `${campoInputBase} border-neutral-300`
|
||||
@@ -40,6 +34,9 @@ export default function CotizadorPage() {
|
||||
const { unidades, cargando, error, usandoSeed } = useUnidades()
|
||||
const cotizacion = useCotizacion()
|
||||
const {
|
||||
desarrollo,
|
||||
setDesarrollo,
|
||||
params,
|
||||
unidad,
|
||||
setUnidad,
|
||||
acabados,
|
||||
@@ -52,6 +49,9 @@ export default function CotizadorPage() {
|
||||
propuestas,
|
||||
} = cotizacion
|
||||
|
||||
const unidadesDesarrollo = unidades.filter((u) => (u.desarrollo ?? 'DIAGONAL') === desarrollo)
|
||||
const disponiblesDesarrollo = unidadesDesarrollo.filter((u) => u.status === 'DISPONIBLE').length
|
||||
|
||||
const {
|
||||
register,
|
||||
watch,
|
||||
@@ -72,7 +72,7 @@ export default function CotizadorPage() {
|
||||
email: watch('email') || undefined,
|
||||
telefono: watch('telefono') || undefined,
|
||||
}
|
||||
generarPdf({ unidad, acabados, apartado, precioLista, propuestas, cliente })
|
||||
generarPdf({ unidad, acabados, apartado, precioLista, propuestas, cliente, desarrollo: params })
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -105,15 +105,51 @@ export default function CotizadorPage() {
|
||||
{cargando ? (
|
||||
<p className="text-sm text-neutral-500">Cargando unidades…</p>
|
||||
) : (
|
||||
<UnitSelector
|
||||
unidades={unidades}
|
||||
valor={unidad?.codigo ?? ''}
|
||||
onChange={(codigo) =>
|
||||
setUnidad(unidades.find((u) => u.codigo === codigo) ?? null)
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<div>
|
||||
<span className={etiquetaCampo}>Desarrollo</span>
|
||||
<div className="flex flex-wrap border border-neutral-300" role="group">
|
||||
{LISTA_DESARROLLOS.map((id) => {
|
||||
const activo = id === desarrollo
|
||||
const total = unidades.filter((u) => (u.desarrollo ?? 'DIAGONAL') === id).length
|
||||
const disponibles = unidades.filter(
|
||||
(u) => (u.desarrollo ?? 'DIAGONAL') === id && u.status === 'DISPONIBLE',
|
||||
).length
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
aria-pressed={activo}
|
||||
onClick={() => setDesarrollo(id)}
|
||||
className={`px-4 py-2.5 text-xs font-bold tracking-widest uppercase transition-colors duration-150 ${
|
||||
activo
|
||||
? 'bg-accent text-white'
|
||||
: 'bg-white text-neutral-600 hover:bg-neutral-100'
|
||||
}`}
|
||||
>
|
||||
{obtenerDesarrollo(id).nombre}
|
||||
<span className={`ml-1 font-normal ${activo ? 'text-white' : 'text-neutral-400'}`}>
|
||||
{disponibles}/{total}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-neutral-500">
|
||||
{unidadesDesarrollo.length} unidades · {disponiblesDesarrollo} disponibles
|
||||
</p>
|
||||
</div>
|
||||
<UnitSelector
|
||||
unidades={unidadesDesarrollo}
|
||||
valor={unidad?.codigo ?? ''}
|
||||
onChange={(codigo) =>
|
||||
setUnidad(unidadesDesarrollo.find((u) => u.codigo === codigo) ?? null)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!params.precioUnico && (
|
||||
<div>
|
||||
<span className={etiquetaCampo}>Cocina y closet</span>
|
||||
<div className="inline-flex border border-neutral-300" role="group">
|
||||
@@ -137,6 +173,7 @@ export default function CotizadorPage() {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="apartado" className={etiquetaCampo}>
|
||||
@@ -227,9 +264,9 @@ export default function CotizadorPage() {
|
||||
<ProposalCard
|
||||
key={p.tipo}
|
||||
numero={i + 1}
|
||||
titulo={TITULOS_PROPUESTAS[i][0]}
|
||||
titulo={p.titulo}
|
||||
propuesta={p}
|
||||
onDescuentoChange={(v) => setDescuento(TITULOS_PROPUESTAS[i][1], v)}
|
||||
onDescuentoChange={(v) => setDescuento(params.propuestas[i].claveDescuento, v)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -237,7 +274,7 @@ export default function CotizadorPage() {
|
||||
precioLista={precioLista}
|
||||
apartado={apartado}
|
||||
propuestas={propuestas}
|
||||
titulos={TITULOS_PROPUESTAS.map(([titulo]) => titulo)}
|
||||
titulos={propuestas.map((p) => p.titulo)}
|
||||
/>
|
||||
<div className="mt-16 flex justify-end">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user