feat: esquema unico de cotizacion FRASA 2026 (reemplaza las 4 propuestas)
Flujo aprobado por el cliente (28 sep 2026), porcentajes globales para todos los desarrollos: - Apartado: cantidad libre (sin minimo), se acredita dentro del enganche de firma; el excedente se descuenta del enganche - Enganche de firma: 2.5% del precio (saldo a firma = max(0, 2.5% - apartado)) - Enganche: 7.5%, diferible en N mensualidades (N a eleccion del vendedor, 1-36) - Gastos de escrituracion: 5.5%, pago unico a la firma o diferido con el enganche (toggle) - lib/esquema-frasa.ts (frontend) + cotizacion-esquema.service.ts (backend): modulos puros espejo, caso dorado M/apartado 5k/N=14 en tests de ambos - Migracion 20260928120000: 7 columnas nuevas en Cotizacion (esquema, saldoFirma, engancheDiferible, escrituracion, etc.); legacy se rellena para compatibilidad de reportes (tipoPropuesta=FRASA_2026) - UI: panel de linea de tiempo (Hoy -> Firma -> Mensualidades) sustituye a propuestas/comparadores; PDF y WhatsApp con el esquema unico - API recalcula server-side; smoke end-to-end verificado contra inventario - 743 tests API + 128 cotizador en verde
This commit is contained in:
164
apps/cotizador/src/components/EsquemaPanel.tsx
Normal file
164
apps/cotizador/src/components/EsquemaPanel.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import {
|
||||
MENSUALIDADES_MAX,
|
||||
MENSUALIDADES_MIN,
|
||||
PCT_ENGANCHE,
|
||||
PCT_ENGANCHE_FIRMA,
|
||||
PCT_ESCRITURACION,
|
||||
type EsquemaFrasaResultado,
|
||||
} from '../lib/esquema-frasa'
|
||||
import { formatoMoneda, formatoPorcentaje } from '../lib/formatters'
|
||||
|
||||
interface Props {
|
||||
/** Esquema FRASA 2026 calculado (siempre presente: solo se renderiza con unidad) */
|
||||
esquema: EsquemaFrasaResultado
|
||||
numMensualidades: number
|
||||
onNumMensualidades: (n: number) => void
|
||||
escrituracionDiferida: boolean
|
||||
onEscrituracionDiferida: (v: boolean) => void
|
||||
}
|
||||
|
||||
const pasoBase =
|
||||
'flex-1 rounded-xl border border-[#E5E7EB] bg-white p-4 dark:border-[#1E293B] dark:bg-[#0F172A]'
|
||||
const pasoTitulo = 'text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]'
|
||||
const pasoMonto = 'mt-1 text-lg font-semibold text-[#111827] dark:text-[#E2E8F0]'
|
||||
|
||||
/**
|
||||
* Panel único del esquema FRASA 2026 (reemplaza a las 4 propuestas):
|
||||
* línea de tiempo Hoy → A la firma → Mensualidades, el detalle de conceptos
|
||||
* (enganche de firma 2.5%, enganche 7.5%, escrituración 5.5%) y los controles
|
||||
* del diferimiento: número de mensualidades (1..36) y si la escrituración se
|
||||
* difiere con el enganche o se paga a la firma.
|
||||
*/
|
||||
export function EsquemaPanel({
|
||||
esquema,
|
||||
numMensualidades,
|
||||
onNumMensualidades,
|
||||
escrituracionDiferida,
|
||||
onEscrituracionDiferida,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Línea de tiempo del esquema */}
|
||||
<div className="flex flex-col gap-4 sm:flex-row">
|
||||
<div className={pasoBase}>
|
||||
<p className={pasoTitulo}>Hoy</p>
|
||||
<p className={pasoMonto}>{formatoMoneda(esquema.apartado)}</p>
|
||||
<p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">Apartado (aparta la unidad)</p>
|
||||
</div>
|
||||
<div className={pasoBase}>
|
||||
<p className={pasoTitulo}>A la firma</p>
|
||||
<p className={pasoMonto}>{formatoMoneda(esquema.pagoUnicoFirma)}</p>
|
||||
<p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">
|
||||
Saldo de enganche de firma
|
||||
{!escrituracionDiferida && ' + escrituración'}
|
||||
</p>
|
||||
</div>
|
||||
<div className={pasoBase}>
|
||||
<p className={pasoTitulo}>Después</p>
|
||||
<p className={pasoMonto}>
|
||||
{esquema.numMensualidades} × {formatoMoneda(esquema.mensualidad)}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">
|
||||
Total diferible {formatoMoneda(esquema.baseDiferible)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detalle de conceptos */}
|
||||
<section className="rounded-xl border border-[#E5E7EB] bg-white p-6 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm">
|
||||
<h2 className="mb-4 text-xs font-semibold uppercase tracking-wider text-[#111827] dark:text-[#E2E8F0]">
|
||||
Esquema FRASA 2026 — conceptos
|
||||
</h2>
|
||||
<dl className="divide-y divide-[#E5E7EB] dark:divide-[#334155]">
|
||||
<div className="flex flex-col gap-1 py-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<dt className="text-sm text-[#64748B] dark:text-[#94A3B8]">
|
||||
Enganche de firma ({formatoPorcentaje(PCT_ENGANCHE_FIRMA)})
|
||||
</dt>
|
||||
<dd className="text-sm font-medium text-[#111827] dark:text-[#E2E8F0]">
|
||||
{formatoMoneda(esquema.engancheFirmaTotal)}
|
||||
<span className="ml-2 text-xs font-normal text-[#64748B] dark:text-[#94A3B8]">
|
||||
− apartado acreditado → saldo a la firma {formatoMoneda(esquema.saldoFirma)}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
{esquema.excedenteApartado > 0 && (
|
||||
<div className="flex flex-col gap-1 py-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<dt className="text-sm text-[#64748B] dark:text-[#94A3B8]">
|
||||
Apartado que excede el 2.5%
|
||||
</dt>
|
||||
<dd className="text-sm font-medium text-[#111827] dark:text-[#E2E8F0]">
|
||||
{formatoMoneda(esquema.excedenteApartado)}
|
||||
<span className="ml-2 text-xs font-normal text-[#64748B] dark:text-[#94A3B8]">
|
||||
acreditado al enganche
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-1 py-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<dt className="text-sm text-[#64748B] dark:text-[#94A3B8]">
|
||||
Enganche ({formatoPorcentaje(PCT_ENGANCHE)}) diferible
|
||||
</dt>
|
||||
<dd className="text-sm font-medium text-[#111827] dark:text-[#E2E8F0]">
|
||||
{formatoMoneda(esquema.enganche)}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 py-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<dt className="text-sm text-[#64748B] dark:text-[#94A3B8]">
|
||||
Gastos de escrituración ({formatoPorcentaje(PCT_ESCRITURACION)})
|
||||
</dt>
|
||||
<dd className="text-sm font-medium text-[#111827] dark:text-[#E2E8F0]">
|
||||
{formatoMoneda(esquema.escrituracion)}
|
||||
<span className="ml-2 text-xs font-normal text-[#64748B] dark:text-[#94A3B8]">
|
||||
{escrituracionDiferida ? 'diferidos con el enganche' : 'pagados a la firma'}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 py-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<dt className="text-sm font-semibold text-[#111827] dark:text-[#E2E8F0]">
|
||||
Total del esquema (15.5% del precio)
|
||||
</dt>
|
||||
<dd className="text-sm font-semibold text-[#111827] dark:text-[#E2E8F0]">
|
||||
{formatoMoneda(esquema.totalPagado)}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
{/* Controles del diferimiento */}
|
||||
<section className="flex flex-col gap-4 rounded-xl border border-[#E5E7EB] bg-white p-6 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm sm:flex-row sm:items-end">
|
||||
<div className="flex-1">
|
||||
<label htmlFor="mensualidades-esquema" className="mb-2 block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]">
|
||||
Mensualidades del diferimiento
|
||||
</label>
|
||||
<select
|
||||
id="mensualidades-esquema"
|
||||
value={numMensualidades}
|
||||
onChange={(e) => onNumMensualidades(Number(e.target.value))}
|
||||
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:ring-2 focus:ring-accent/20 focus:outline-none dark:bg-[#0F172A] dark:border-[#334155] dark:text-[#E2E8F0]"
|
||||
>
|
||||
{Array.from({ length: MENSUALIDADES_MAX - MENSUALIDADES_MIN + 1 }, (_, i) => MENSUALIDADES_MIN + i).map(
|
||||
(n) => (
|
||||
<option key={n} value={n}>
|
||||
{n} mensualidades
|
||||
</option>
|
||||
),
|
||||
)}
|
||||
</select>
|
||||
<p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">
|
||||
De {MENSUALIDADES_MIN} a {MENSUALIDADES_MAX}; cada una de {formatoMoneda(esquema.mensualidad)}
|
||||
</p>
|
||||
</div>
|
||||
<label className="flex cursor-pointer items-center gap-2 pb-2.5 text-sm text-[#374151] dark:text-[#CBD5E1]">
|
||||
<input
|
||||
id="escrituracion-diferida"
|
||||
type="checkbox"
|
||||
checked={escrituracionDiferida}
|
||||
onChange={(e) => onEscrituracionDiferida(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-[#D1D5DB] accent-accent"
|
||||
/>
|
||||
Diferir escrituración con el enganche
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user