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,69 @@
import { useFormContext } from 'react-hook-form'
import { CampoTexto, CampoSelect } from '../components/Campos'
import SectionCard from '../components/SectionCard'
import { compararConDatos } from '../lib/curp'
import { ESTADOS_CIVILES, REGIMENES_BIENES, TIPOS_VIVIENDA, TIPOS_COMPRA } from '../data/constantes'
interface Props {
prefix: string
compacto?: boolean
}
/** Campos de datos generales; reutilizable con prefijo (cliente y co-acreditado) */
export default function DatosGeneralesCampos({ prefix, compacto = false }: Props) {
const { watch } = useFormContext()
const p = (campo: string) => `${prefix}${campo}`
const estadoCivil = watch(p('estadoCivil')) as string
const curp = (watch(p('curp')) as string) ?? ''
const fechaNacimiento = (watch(p('fechaNacimiento')) as string) ?? ''
const lugarNacimiento = (watch(p('lugarNacimiento')) as string) ?? ''
// Advertencias ámbar en vivo: no bloquean, solo alertan inconsistencias con la CURP
const advertenciasCurp = compararConDatos(curp, { fechaNacimiento, lugarNacimiento })
return (
<SectionCard titulo="Datos generales">
{!compacto && <CampoTexto nombre={p('campaniaVenta')} etiqueta="Campaña de la venta" />}
<CampoTexto nombre={p('fecha')} etiqueta="Fecha" tipo="date" requerido={!compacto} soloLectura={compacto} />
<CampoTexto nombre={p('nombreCompleto')} etiqueta="Nombre completo" requerido={!compacto} className="sm:col-span-2" />
<CampoTexto nombre={p('nacionalidad')} etiqueta="Nacionalidad" />
<CampoTexto nombre={p('nivelEstudios')} etiqueta="Nivel de estudios" />
<CampoTexto nombre={p('curp')} etiqueta="CURP" requerido={!compacto} placeholder="XXXX000000XXXXXXX0" />
{advertenciasCurp.length > 0 && (
<div role="alert" className="border border-amber-300 bg-amber-50 px-4 py-3 sm:col-span-2">
{advertenciasCurp.map((aviso) => (
<p key={aviso} className="text-xs text-amber-700">
{aviso}
</p>
))}
</div>
)}
<CampoTexto nombre={p('rfc')} etiqueta="RFC" requerido={!compacto} placeholder="XXXX000000XXX" />
<CampoTexto nombre={p('nss')} etiqueta="NSS" />
<CampoTexto nombre={p('delegacionMunicipio')} etiqueta="Delegación / Municipio" />
<CampoTexto nombre={p('direccion')} etiqueta="Dirección" className="sm:col-span-2" />
<CampoTexto nombre={p('codigoPostal')} etiqueta="Código postal" placeholder="5 dígitos" />
<CampoTexto nombre={p('antiguedadDomicilio')} etiqueta="Antigüedad en domicilio (años)" />
<CampoTexto nombre={p('fechaNacimiento')} etiqueta="Fecha de nacimiento" tipo="date" requerido={!compacto} />
<CampoTexto nombre={p('lugarNacimiento')} etiqueta="Lugar de nacimiento" />
<CampoSelect nombre={p('estadoCivil')} etiqueta="Estado civil" opciones={ESTADOS_CIVILES} />
{estadoCivil === 'CASADO' && (
<CampoSelect nombre={p('regimenBienes')} etiqueta="Régimen de bienes" opciones={REGIMENES_BIENES} requerido={!compacto} />
)}
<CampoTexto nombre={p('peso')} etiqueta="Peso (kg)" />
<CampoTexto nombre={p('estatura')} etiqueta="Estatura (m)" />
<CampoTexto nombre={p('email')} etiqueta="Email" tipo="email" requerido={!compacto} />
<CampoTexto nombre={p('telCasa')} etiqueta="Tel. casa" />
<CampoTexto nombre={p('celular')} etiqueta="Celular" requerido={!compacto} />
<CampoTexto nombre={p('viviendaAnios')} etiqueta="Años en vivienda" />
<CampoSelect nombre={p('tipoVivienda')} etiqueta="Tipo de vivienda" opciones={TIPOS_VIVIENDA} />
<CampoSelect nombre={p('tipoCompra')} etiqueta="Tipo de compra" opciones={TIPOS_COMPRA} />
{!compacto && (
<CampoTexto
nombre={p('actividadOcupacion')}
etiqueta="Actividad, ocupación o profesión"
className="sm:col-span-2"
/>
)}
</SectionCard>
)
}