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:
108
apps/alta-clientes/src/components/Campos.tsx
Normal file
108
apps/alta-clientes/src/components/Campos.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user