Overview
SIGEAC’s form components provide a complete solution for data entry with built-in validation, multi-step workflows, and role-based access control. All forms use React Hook Form with Zod validation for type-safe form handling.
A two-step form for creating employees with optional user account creation.
Location : components/forms/general/CreateEmployeeForm.tsx:120
Features
Multi-step workflow (employee data → user account)
Conditional validation based on user creation
Auto-generated usernames
Multi-company location assignment
Role selection with badges
Password visibility toggle
Props
Callback function called after successful form submission
Type Definition
const formSchema = z . object ({
// Employee fields
first_name: z . string (). min ( 1 , "Requerido" ),
middle_name: z . string (). optional (),
last_name: z . string (). min ( 1 , "Requerido" ),
second_last_name: z . string (). optional (),
dni_type: z . string (),
blood_type: z . string (),
dni: z . string (). min ( 6 , "Requerido" ),
department_id: z . string (),
job_title_id: z . string (),
location_id: z . string (),
// User account fields (conditional)
createUser: z . boolean (),
username: z . string (). min ( 3 , "Mínimo 3 caracteres" ). optional (),
password: z . string (). min ( 5 , "Mínimo 5 caracteres" ). optional (),
email: z . string (). email ( "Correo inválido" ). optional (),
roles: z . array ( z . string ()). optional (),
companies_locations: z . array ( z . object ({
companyID: z . number (),
locationID: z . array ( z . number (). or ( z . string ())),
})). optional (),
}). superRefine (( data , ctx ) => {
// Conditional validation when createUser is true
if ( data . createUser ) {
if ( ! data . username ) {
ctx . addIssue ({
code: z . ZodIssueCode . custom ,
message: "Requerido" ,
path: [ "username" ],
});
}
// Additional validations...
}
});
type EmployeeForm = z . infer < typeof formSchema >;
Usage Example
import { CreateEmployeeForm } from '@/components/forms/general/CreateEmployeeForm' ;
export function EmployeePage () {
const handleSuccess = () => {
console . log ( 'Employee created successfully' );
// Refresh data, show notification, etc.
};
return (
< div className = "p-6" >
< h1 className = "text-2xl font-bold mb-4" > Nuevo Empleado </ h1 >
< CreateEmployeeForm onSuccess = { handleSuccess } />
</ div >
);
}
SIGEAC uses a step-based pattern for complex forms:
const [ step , setStep ] = useState < 1 | 2 > ( 1 );
const handleNextStep = () => {
if ( form . watch ( "createUser" )) {
setStep ( 2 ); // Advance to user creation
} else {
form . handleSubmit ( onSubmit )(); // Submit immediately
}
};
return (
< Form { ... form } >
< form onSubmit = { form . handleSubmit ( onSubmit ) } >
{ step === 1 && (
<>
{ /* Employee fields */ }
< Button onClick = { handleNextStep } >
{ form . watch ( "createUser" ) ? "Siguiente" : "Crear Empleado" }
</ Button >
</>
) }
{ step === 2 && (
<>
{ /* User account fields */ }
< Button onClick = { () => setStep ( 1 ) } > Anterior </ Button >
< Button type = "submit" > Crear Empleado y Usuario </ Button >
</>
) }
</ form >
</ Form >
);
All forms use consistent field patterns:
Text Input
Select
Checkbox
Multi-Select
< FormField
control = { form . control }
name = "first_name"
render = { ({ field }) => (
< FormItem >
< FormLabel > Nombre </ FormLabel >
< FormControl >
< Input placeholder = "Ej. Juan" { ... field } />
</ FormControl >
< FormMessage />
</ FormItem >
) }
/>
< FormField
control = { form . control }
name = "department_id"
render = { ({ field }) => (
< FormItem >
< FormLabel > Departamento </ FormLabel >
< Select onValueChange = { field . onChange } >
< FormControl >
< SelectTrigger >
< SelectValue placeholder = "Selecciona un departamento" />
</ SelectTrigger >
</ FormControl >
< SelectContent >
{ departments ?. map (( dept ) => (
< SelectItem key = { dept . id } value = { dept . id . toString () } >
{ dept . name }
</ SelectItem >
)) }
</ SelectContent >
</ Select >
< FormMessage />
</ FormItem >
) }
/>
< FormField
control = { form . control }
name = "createUser"
render = { ({ field }) => (
< FormItem className = "flex items-center gap-2 p-4 border rounded-lg" >
< FormControl >
< Checkbox
checked = { field . value }
onCheckedChange = { field . onChange }
/>
</ FormControl >
< FormLabel className = "!mt-0" >
¿Crear usuario para este empleado?
</ FormLabel >
</ FormItem >
) }
/>
< FormField
control = { form . control }
name = "roles"
render = { ({ field }) => (
< FormItem >
< FormLabel > Roles </ FormLabel >
< Popover open = { openRoles } onOpenChange = { setOpenRoles } >
< PopoverTrigger asChild >
< Button variant = "outline" className = "w-full justify-between" >
{ selectedRoles . length > 0 ? (
< div className = "flex gap-1 flex-wrap" >
{ roles ?. filter ( role =>
selectedRoles . includes ( role . id . toString ())
). map ( role => (
< Badge key = { role . id } > { role . name } </ Badge >
)) }
</ div >
) : "Seleccione roles..." }
< ChevronsUpDown className = "ml-2 h-4 w-4" />
</ Button >
</ PopoverTrigger >
< PopoverContent >
< Command >
< CommandInput placeholder = "Buscar roles..." />
< CommandList >
< CommandGroup >
{ roles ?. map ( role => (
< CommandItem
key = { role . id }
onSelect = { () => handleRoleSelect ( role . id . toString ()) }
>
< Check className = { cn (
"mr-2 h-4 w-4" ,
isRoleSelected ( role . id . toString ())
? "opacity-100"
: "opacity-0"
) } />
{ role . label }
</ CommandItem >
)) }
</ CommandGroup >
</ CommandList >
</ Command >
</ PopoverContent >
</ Popover >
< FormMessage />
</ FormItem >
) }
/>
Auto-Generated Fields
Forms can auto-populate fields based on other inputs:
// Watch form values
const firstName = form . watch ( "first_name" );
const lastName = form . watch ( "last_name" );
const shouldCreateUser = form . watch ( "createUser" );
// Auto-generate username
useEffect (() => {
if ( shouldCreateUser && firstName && lastName ) {
const username = ` ${ firstName . charAt ( 0 ) }${ lastName } ` . toLowerCase ();
form . setValue ( "username" , username );
}
}, [ shouldCreateUser , firstName , lastName , form ]);
Password Visibility Toggle
const [ showPassword , setShowPassword ] = useState ( false );
< FormField
control = { form . control }
name = "password"
render = { ({ field }) => (
< FormItem >
< FormLabel className = "flex items-center gap-2" >
Contraseña
< button
type = "button"
onClick = { () => setShowPassword ( ! showPassword ) }
className = "text-muted-foreground hover:text-primary"
>
{ showPassword ? < EyeOff className = "h-4 w-4" /> : < Eye className = "h-4 w-4" /> }
</ button >
</ FormLabel >
< FormControl >
< Input
type = { showPassword ? "text" : "password" }
placeholder = "Mínimo 5 caracteres"
{ ... field }
/>
</ FormControl >
< FormMessage />
</ FormItem >
) }
/>
Nested Location Selection
Complex nested selection with Accordion:
< ScrollArea className = "h-[250px] w-full" >
< Accordion type = "single" collapsible >
{ companies ?. map ( company => (
< AccordionItem key = { company . id } value = { company . name } >
< AccordionTrigger > { company . name } </ AccordionTrigger >
< AccordionContent >
{ locations
. filter ( loc => loc . company_id === company . id )
. map ( location => (
location . locations . map ( loc => (
< div key = { loc . id } className = "flex items-center space-x-2" >
< Checkbox
checked = { Boolean (
field . value ?. find (
item => item . companyID === company . id &&
item . locationID . includes ( loc . id )
)
) }
onCheckedChange = { ( isSelected ) =>
handleLocationChange ( company . id , loc . id , isSelected )
}
/>
< Label > { loc . address } </ Label >
</ div >
))
)) }
</ AccordionContent >
</ AccordionItem >
)) }
</ Accordion >
</ ScrollArea >
Sequential API Calls
Handle dependent API calls in order:
const onSubmit = async ( data : EmployeeForm ) => {
try {
if ( data . createUser ) {
// 1. Create user first
const userResponse = await createUser . mutateAsync ({
username: data . username ! ,
password: data . password ,
email: data . email ! ,
roles: data . roles ?. map ( Number ) || [],
companies_locations: data . companies_locations ,
});
// 2. Create employee with user_id
await createEmployee . mutateAsync ({
... employeeData ,
user_id: userResponse . user . id ,
});
} else {
// Create employee without user
await createEmployee . mutateAsync ( employeeData );
}
onSuccess ?.();
} catch ( error ) {
console . error ( "Error creating employee:" , error );
}
};
Loading States
< Button
type = "submit"
disabled = { createEmployee . isPending || createUser . isPending }
>
{ createEmployee . isPending || createUser . isPending ? (
<>
< Loader2 className = "mr-2 h-4 w-4 animate-spin" />
Creando...
</>
) : (
"Crear Empleado y Usuario"
) }
</ Button >
Best Practices
Always define schemas with Zod for type-safe validation. Use .superRefine() for conditional validation logic.
Disable submit buttons during mutations and show loading indicators with Loader2 icon.
Provide clear error messages
Use Spanish language error messages that guide users on how to fix issues.
Auto-populate when possible
Use useEffect to auto-generate fields like usernames based on other inputs.
Reset forms after success
Call onSuccess callback to close dialogs or navigate away after successful submission.
Dialogs - Wrap forms in modal dialogs
Tables - Display form submissions in data tables