- 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
109 lines
3.4 KiB
TypeScript
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>
|
|
)
|
|
}
|