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:
112
apps/alta-clientes/src/forms/ViviendaCreditoForm.tsx
Normal file
112
apps/alta-clientes/src/forms/ViviendaCreditoForm.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user