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
165 lines
7.5 KiB
TypeScript
165 lines
7.5 KiB
TypeScript
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>
|
||
)
|
||
}
|