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:
@@ -21,7 +21,8 @@ export function ComparadorPropuestas({ precioLista, apartado, propuestas, titulo
|
||||
const [abierto, setAbierto] = useState(false)
|
||||
|
||||
const totales = propuestas.map((p) => totalPagado(p, apartado))
|
||||
const ganador = indiceMenorTotal(totales)
|
||||
// null (plazo desconocido) nunca gana el menor total
|
||||
const ganador = indiceMenorTotal(totales.map((t) => t ?? Number.POSITIVE_INFINITY))
|
||||
|
||||
const filas: Fila[] = [
|
||||
{ etiqueta: 'Precio de lista', obtener: () => formatoMoneda(precioLista) },
|
||||
@@ -34,19 +35,30 @@ export function ComparadorPropuestas({ precioLista, apartado, propuestas, titulo
|
||||
{
|
||||
etiqueta: 'Mensualidad',
|
||||
obtener: (p) =>
|
||||
p.mensualidad === null
|
||||
? 'N/A'
|
||||
: `${p.numMensualidades} × ${formatoMoneda(p.mensualidad)}`,
|
||||
p.notaMensualidades
|
||||
? `— (${p.notaMensualidades})`
|
||||
: p.mensualidad === null
|
||||
? 'N/A'
|
||||
: `${p.numMensualidades} × ${formatoMoneda(p.mensualidad)}`,
|
||||
},
|
||||
{
|
||||
etiqueta: 'Total mensualidades',
|
||||
obtener: (p) => (p.mensualidadesTotal === null ? 'N/A' : formatoMoneda(p.mensualidadesTotal)),
|
||||
obtener: (p) =>
|
||||
p.notaMensualidades
|
||||
? '—'
|
||||
: p.mensualidadesTotal === null
|
||||
? 'N/A'
|
||||
: formatoMoneda(p.mensualidadesTotal),
|
||||
},
|
||||
{ etiqueta: 'Pago final', obtener: (p) => formatoMoneda(p.pagoFinal) },
|
||||
{ etiqueta: 'Total pagado', obtener: (_p, i) => formatoMoneda(totales[i]), destacada: true },
|
||||
{
|
||||
etiqueta: 'Total pagado',
|
||||
obtener: (_p, i) => (totales[i] === null ? '—' : formatoMoneda(totales[i])),
|
||||
destacada: true,
|
||||
},
|
||||
{
|
||||
etiqueta: 'Diferencia vs precio de lista',
|
||||
obtener: (_p, i) => formatoMoneda(totales[i] - precioLista),
|
||||
obtener: (_p, i) => (totales[i] === null ? '—' : formatoMoneda(totales[i] - precioLista)),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -67,7 +67,9 @@ export function ProposalCard({ numero, titulo, propuesta, onDescuentoChange }: P
|
||||
nota="incluye deducción del apartado"
|
||||
valor={formatoMoneda(propuesta.enganche)}
|
||||
/>
|
||||
{propuesta.mensualidad !== null && propuesta.mensualidadesTotal !== null ? (
|
||||
{propuesta.notaMensualidades ? (
|
||||
<Renglon etiqueta="Mensualidades" nota={propuesta.notaMensualidades} valor="—" />
|
||||
) : propuesta.mensualidad !== null && propuesta.mensualidadesTotal !== null ? (
|
||||
<Renglon
|
||||
etiqueta="Mensualidades"
|
||||
nota={`${propuesta.numMensualidades} × ${formatoMoneda(propuesta.mensualidad)}`}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { formatoFechaCorta } from '../lib/formatters'
|
||||
|
||||
interface Props {
|
||||
unidad: Unidad
|
||||
numMeses: number
|
||||
numMeses: number | null
|
||||
}
|
||||
|
||||
function Fila({ etiqueta, valor }: { etiqueta: string; valor: string }) {
|
||||
@@ -15,8 +15,12 @@ function Fila({ etiqueta, valor }: { etiqueta: string; valor: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
const formatoM2 = (v: number | null): string =>
|
||||
v === null ? '—' : `${v.toLocaleString('es-MX')} m²`
|
||||
|
||||
/** Tarjeta con la ficha técnica de la unidad seleccionada */
|
||||
export function UnitInfoCard({ unidad, numMeses }: Props) {
|
||||
const extras = unidad.extras ?? {}
|
||||
return (
|
||||
<section className="border border-neutral-200 bg-white p-6">
|
||||
<h2 className="mb-4 text-xs font-bold tracking-widest text-neutral-900 uppercase">
|
||||
@@ -24,11 +28,19 @@ export function UnitInfoCard({ unidad, numMeses }: Props) {
|
||||
</h2>
|
||||
<dl className="divide-y divide-neutral-200">
|
||||
<Fila etiqueta="Modelo" valor={unidad.modelo} />
|
||||
<Fila etiqueta="Sup. construcción" valor={`${unidad.supConstruccion.toLocaleString('es-MX')} m²`} />
|
||||
<Fila etiqueta="Sup. excedente" valor={`${unidad.supExcedente.toLocaleString('es-MX')} m²`} />
|
||||
<Fila etiqueta="Sup. estacionamiento" valor={`${unidad.supEstacionamiento.toLocaleString('es-MX')} m²`} />
|
||||
<Fila etiqueta="Fecha de entrega (obra)" valor={formatoFechaCorta(unidad.fechaEntrega)} />
|
||||
<Fila etiqueta="Meses restantes" valor={String(numMeses)} />
|
||||
{extras['prototipo'] != null && <Fila etiqueta="Prototipo" valor={String(extras['prototipo'])} />}
|
||||
{extras['etapa'] != null && <Fila etiqueta="Etapa" valor={String(extras['etapa'])} />}
|
||||
{extras['manzana'] != null && <Fila etiqueta="Manzana" valor={String(extras['manzana'])} />}
|
||||
{extras['lote'] != null && <Fila etiqueta="Lote" valor={String(extras['lote'])} />}
|
||||
{extras['cajones'] != null && <Fila etiqueta="Cajones" valor={String(extras['cajones'])} />}
|
||||
<Fila etiqueta="Sup. construcción" valor={formatoM2(unidad.supConstruccion)} />
|
||||
<Fila etiqueta="Sup. excedente" valor={formatoM2(unidad.supExcedente)} />
|
||||
<Fila etiqueta="Sup. estacionamiento" valor={formatoM2(unidad.supEstacionamiento)} />
|
||||
<Fila
|
||||
etiqueta="Fecha de entrega (obra)"
|
||||
valor={unidad.fechaEntrega ? formatoFechaCorta(unidad.fechaEntrega) : '—'}
|
||||
/>
|
||||
<Fila etiqueta="Meses restantes" valor={numMeses === null ? '—' : String(numMeses)} />
|
||||
</dl>
|
||||
</section>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user