feat: ecosistema FRASA — apps Cotizador, Alta Clientes, Cobranza y Landing

- Cotizador (/quote): 4 propuestas con formulas validadas del Excel, comparador,
  combobox de unidades, vigencia, PDF ANEXO A
- Alta Clientes (/client): wizard 8 pasos, validacion CURP RENAPO, OCR de INE,
  checklist de expediente, firma digital, 5 PDFs legales
- Cobranza (/pay): planes de enganche, mora, vista cobranza del dia, recibos PDF,
  bitacora, dashboard con filtros
- Landing (/): entrada al ecosistema
- Design system StudioBlank (monocromo, acento #1B4332) en las 4 apps
- 123 tests vitest
This commit is contained in:
2026-08-21 22:27:36 +00:00
commit ecd4edac02
147 changed files with 27345 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
import { useForm, FormProvider } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import type { ViviendaCredito, TipoCredito } from '../types'
import { viviendaCreditoSchema } from '../lib/schemas'
import { CampoTexto, CampoSelect, MensajeError } from '../components/Campos'
import SectionCard from '../components/SectionCard'
import Navegacion from './Navegacion'
import type { PasoProps } from './DatosGeneralesForm'
import { PROYECTOS, TIPOS_CREDITO, TIPOS_PRODUCTO } from '../data/constantes'
/** Paso 6: datos de la vivienda, crédito e identificación oficial (PLD) */
export default function ViviendaCreditoForm({ valores, onAnterior, onSiguiente }: PasoProps<ViviendaCredito>) {
const metodos = useForm<ViviendaCredito>({
resolver: zodResolver(viviendaCreditoSchema),
defaultValues: valores,
mode: 'onSubmit',
})
const proyecto = metodos.watch('proyecto')
const tiposSeleccionados = metodos.watch('tiposCredito') ?? []
const alternarCredito = (valor: TipoCredito) => {
const actuales = metodos.getValues('tiposCredito') ?? []
metodos.setValue(
'tiposCredito',
actuales.includes(valor) ? actuales.filter((t) => t !== valor) : [...actuales, valor],
{ shouldDirty: true },
)
}
return (
<FormProvider {...metodos}>
<form onSubmit={metodos.handleSubmit(onSiguiente)} noValidate>
<div className="space-y-16">
<SectionCard titulo="Datos vivienda">
<CampoSelect
nombre="proyecto"
etiqueta="Proyecto"
requerido
opciones={PROYECTOS.map((p) => ({ valor: p, etiqueta: p === 'OTRO' ? 'Otro' : p }))}
/>
{proyecto === 'OTRO' && (
<CampoTexto nombre="proyectoOtro" etiqueta="Nombre del proyecto" requerido />
)}
<CampoTexto nombre="modelo" etiqueta="Modelo" />
<CampoTexto nombre="manzana" etiqueta="Manzana" />
<CampoTexto nombre="lote" etiqueta="Lote" />
<CampoTexto nombre="ubicacion" etiqueta="Ubicación" />
<CampoTexto nombre="superficieM2" etiqueta="Superficie (m²)" />
<CampoTexto nombre="valorVenta" etiqueta="Valor venta ($)" />
<CampoTexto nombre="enganche" etiqueta="Enganche ($)" />
<CampoTexto nombre="equipamientoAdicional" etiqueta="Equipamiento adicional" />
<CampoTexto nombre="fechaPagoEnganche" etiqueta="Fecha pago de enganche" tipo="date" />
<CampoTexto nombre="firmaContrato" etiqueta="Firma de contrato" tipo="date" />
</SectionCard>
<SectionCard titulo="Tipo de crédito">
<div className="sm:col-span-2 lg:col-span-3">
<div className="flex flex-wrap gap-2">
{TIPOS_CREDITO.map((tc) => (
<label
key={tc.valor}
htmlFor={`tipoCredito-${tc.valor}`}
className={`cursor-pointer border px-3 py-1.5 text-xs font-bold uppercase tracking-wider transition-colors duration-150 ${
tiposSeleccionados.includes(tc.valor)
? 'border-accent bg-accent text-white'
: 'border-neutral-300 bg-white text-neutral-600 hover:border-neutral-900'
}`}
>
<input
id={`tipoCredito-${tc.valor}`}
type="checkbox"
className="sr-only"
checked={tiposSeleccionados.includes(tc.valor)}
onChange={() => alternarCredito(tc.valor)}
/>
{tc.etiqueta}
</label>
))}
</div>
</div>
<CampoTexto nombre="montoCreditoInfoFovi" etiqueta="Monto de crédito Info o Fovi ($)" />
<CampoTexto nombre="ahorroVoluntarioSubcuenta" etiqueta="Ahorro voluntario o subcuenta ($)" />
<CampoTexto nombre="descuentoMensualInfoFovissste" etiqueta="Desc. mensual Info o Fovissste ($)" />
<CampoSelect nombre="tipoProducto" etiqueta="Tipo de producto" opciones={TIPOS_PRODUCTO} />
</SectionCard>
<SectionCard
titulo="Identificación oficial (PLD)"
descripcion="Datos de la identificación vigente para los formatos de ley anti-lavado."
>
<CampoTexto nombre="idAutoridadEmisora" etiqueta="Autoridad que la emite" className="sm:col-span-2" />
<CampoTexto nombre="idNumero" etiqueta="Número de identificación" />
<CampoTexto nombre="idVigencia" etiqueta="Vigencia" tipo="date" />
</SectionCard>
<SectionCard titulo="Domicilio fiscal (opcional)" descripcion="Art. 29 y 29-A CFF.">
<CampoTexto nombre="domicilioFiscal.calle" etiqueta="Calle o avenida" className="sm:col-span-2" />
<CampoTexto nombre="domicilioFiscal.numExt" etiqueta="Núm. exterior" />
<CampoTexto nombre="domicilioFiscal.numInt" etiqueta="Núm. interior" />
<CampoTexto nombre="domicilioFiscal.colonia" etiqueta="Colonia" />
<CampoTexto nombre="domicilioFiscal.municipio" etiqueta="Municipio" />
<CampoTexto nombre="domicilioFiscal.estado" etiqueta="Estado" />
<CampoTexto nombre="domicilioFiscal.cp" etiqueta="Código postal" />
<CampoTexto nombre="domicilioFiscal.pais" etiqueta="País" />
</SectionCard>
</div>
<MensajeError nombre="proyectoOtro" />
<Navegacion onAnterior={onAnterior} />
</form>
</FormProvider>
)
}