Files
FRASA-CAS/apps/cotizador/src/components/EsquemaPanel.tsx
Consult AS Technology 61d1b84fc3 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
2026-09-28 13:00:22 +00:00

165 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}