Files
FRASA-CAS/apps/alta-clientes/src/components/Campos.tsx
Consult AS Technology ecd4edac02 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
2026-08-21 22:27:36 +00:00

109 lines
3.4 KiB
TypeScript

import { useFormContext, get } from 'react-hook-form'
export interface CampoBaseProps {
nombre: string
etiqueta: string
requerido?: boolean
className?: string
}
const inputClase =
'w-full border border-neutral-300 bg-white px-3 py-2.5 text-sm uppercase transition-colors duration-150 focus:border-neutral-900 focus:outline-none'
const labelClase = 'mb-1 block text-xs font-bold uppercase tracking-widest text-neutral-500'
function Etiqueta({ etiqueta, requerido }: { etiqueta: string; requerido?: boolean }) {
return (
<span className={labelClase}>
{etiqueta}
{requerido && <span className="text-red-700"> *</span>}
</span>
)
}
/** Clase del input según haya error de validación */
function claseInput(nombre: string, errors: unknown): string {
return get(errors, nombre) ? `${inputClase} border-red-400` : inputClase
}
export function MensajeError({ nombre }: { nombre: string }) {
const {
formState: { errors },
} = useFormContext()
const error = get(errors, nombre)
if (!error) return null
return <p className="mt-1 text-xs text-red-700">{String(error.message)}</p>
}
interface CampoTextoProps extends CampoBaseProps {
tipo?: string
placeholder?: string
soloLectura?: boolean
}
/** Input de texto conectado a react-hook-form (soporta rutas anidadas con punto) */
export function CampoTexto({ nombre, etiqueta, tipo = 'text', placeholder, requerido, soloLectura, className }: CampoTextoProps) {
const { register, formState: { errors } } = useFormContext()
return (
<label htmlFor={nombre} className={`block ${className ?? ''}`}>
<Etiqueta etiqueta={etiqueta} requerido={requerido} />
<input
id={nombre}
type={tipo}
placeholder={placeholder}
readOnly={soloLectura}
className={`${claseInput(nombre, errors)} ${soloLectura ? 'bg-neutral-50' : ''}`}
{...register(nombre)}
/>
<MensajeError nombre={nombre} />
</label>
)
}
interface CampoNumeroProps extends CampoBaseProps {
paso?: string
}
/** Input numérico; cadena vacía se guarda como 0 */
export function CampoNumero({ nombre, etiqueta, requerido, paso = '0.01', className }: CampoNumeroProps) {
const { register, formState: { errors } } = useFormContext()
return (
<label htmlFor={nombre} className={`block ${className ?? ''}`}>
<Etiqueta etiqueta={etiqueta} requerido={requerido} />
<input
id={nombre}
type="number"
step={paso}
min="0"
className={claseInput(nombre, errors)}
{...register(nombre, {
setValueAs: (v: string) => (v === '' ? 0 : Number(v)),
})}
/>
<MensajeError nombre={nombre} />
</label>
)
}
interface CampoSelectProps extends CampoBaseProps {
opciones: { valor: string; etiqueta: string }[]
}
export function CampoSelect({ nombre, etiqueta, opciones, requerido, className }: CampoSelectProps) {
const { register, formState: { errors } } = useFormContext()
return (
<label htmlFor={nombre} className={`block ${className ?? ''}`}>
<Etiqueta etiqueta={etiqueta} requerido={requerido} />
<select id={nombre} className={claseInput(nombre, errors)} {...register(nombre)}>
<option value="">— Selecciona —</option>
{opciones.map((o) => (
<option key={o.valor} value={o.valor}>
{o.etiqueta}
</option>
))}
</select>
<MensajeError nombre={nombre} />
</label>
)
}