Editing Staff
This guide covers all methods for updating staff profiles, including the enhanced 4-tab form system, field updates, and data validation.
Accessing Staff Edit
From Staff List
- Navigate to Dashboard → Staff Management
- Click on a staff member row to view details
- Click the Edit Profile button
// Source: src/components/staff/StaffDetail.tsx:94-100
<button
onClick={() => onEdit(staff)}
className="form-button flex items-center gap-2"
>
<Edit className="w-5 h-5" />
Edit Profile
</button>
From Staff Detail View
The staff detail view displays all information across 5 tabs:
// Source: src/components/staff/StaffDetail.tsx:58-65
const tabs: Array<{ id: TabType; label: string; icon: React.ReactNode }> = [
{ id: 'overview', label: 'Overview', icon: <User className="w-4 h-4" /> },
{ id: 'preferences', label: 'Working Preferences', icon: <Briefcase className="w-4 h-4" /> },
{ id: 'skills', label: 'Skills & Certifications', icon: <Award className="w-4 h-4" /> },
{ id: 'availability', label: 'Availability', icon: <CalendarDays className="w-4 h-4" /> },
{ id: 'constraints', label: 'Constraints', icon: <AlertCircle className="w-4 h-4" /> },
]
Enhanced Staff Form
4-Tab Edit Form
The staff edit form uses the same enhanced form component as creation:
// Source: src/components/staff/EnhancedStaffForm.tsx:477-483
<Tabs defaultValue="basic" className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="basic">Basic Info</TabsTrigger>
<TabsTrigger value="preferences">Working Preferences</TabsTrigger>
<TabsTrigger value="skills">Skills & Certifications</TabsTrigger>
<TabsTrigger value="availability">Availability</TabsTrigger>
</TabsList>
Edit Mode Detection
// Source: src/components/staff/EnhancedStaffForm.tsx:362-365
if (staff) {
// Update existing staff using enhanced API
const result = await updateStaffWithCompleteData(companyId, staff.id, {
// ... update data
})
}
Tab 1: Basic Information
Editable Fields
| Field | Type | Required | Validation |
|---|---|---|---|
| First Name | Text | ✅ Yes | Min 1 character |
| Last Name | Text | ✅ Yes | Min 1 character |
| ✅ Yes | Valid email format | ||
| Phone | Tel | ✅ Yes | Valid phone format |
| Address | Text | ✅ Yes | Max 200 characters |
| Date of Birth | Date | ✅ Yes | YYYY-MM-DD format |
| Role | Select | ✅ Yes | Valid role UUID |
| Start Date | Date | ✅ Yes | YYYY-MM-DD format |
| Status | Select | ✅ Yes | Active/Inactive |
| Contract Hours | Number | ✅ Yes | 0-80 hours |
| Pay Type | Select | ❌ No | hourly/weekly/monthly/annual |
| Hourly Rate | Currency | ❌ No | If pay type is hourly |
| Salary Amount | Currency | ❌ No | If pay type is salary |
| Employment Type | Select | ✅ Yes | full-time/part-time/contract/temp |
| Notes | Textarea | ❌ No | Free text |
Phone Validation
// Source: src/components/staff/EnhancedStaffForm.tsx:29
const phoneRegex = /^\+?[1-9][\d\s\-().]{0,20}$/
Date Validation
// Source: src/components/staff/EnhancedStaffForm.tsx:44-50
date_of_birth: z.string()
.regex(/^\d{4}-\d{2}-\d{2}$/, 'Date of birth must use YYYY-MM-DD format'),
employment_start_date: z.string()
.regex(/^\d{4}-\d{2}-\d{2}$/, 'Employment start date must use YYYY-MM-DD format'),
Tab 2: Working Preferences
Editable Preferences
// Source: src/components/staff/EnhancedStaffForm.tsx:66-78
working_preferences: z.object({
preferred_start_time: z.string().optional(),
preferred_end_time: z.string().optional(),
min_hours_per_week: z.number().min(0).max(168).optional(),
max_hours_per_week: z.number().min(0).max(168).optional(),
min_hours_per_day: z.number().min(0).max(24).optional(),
max_hours_per_day: z.number().min(0).max(24).optional(),
max_consecutive_days: z.number().min(1).max(14).optional(),
min_days_off_between: z.number().min(0).max(7).optional(),
weekend_availability: z.boolean().optional(),
overtime_willing: z.boolean().optional(),
preferred_shift_types: z.array(z.string()).optional()
}).optional()
Preference Fields
| Preference | Type | Range | Description |
|---|---|---|---|
| Preferred Start | Time | 00:00-23:59 | Ideal shift start time |
| Preferred End | Time | 00:00-23:59 | Ideal shift end time |
| Min Hours/Week | Number | 0-168 | Minimum weekly hours |
| Max Hours/Week | Number | 0-168 | Maximum weekly hours |
| Min Hours/Day | Number | 0-24 | Minimum daily hours |
| Max Hours/Day | Number | 0-24 | Maximum daily hours |
| Max Consecutive Days | Number | 1-14 | Maximum days in a row |
| Days Off Between | Number | 0-7 | Required rest days |
| Weekend Available | Boolean | Yes/No | Can work weekends |
| Overtime Willing | Boolean | Yes/No | Will work overtime |
| Shift Types | Multi-select | - | Preferred shift types |
Default Values
// Source: src/components/staff/EnhancedStaffForm.tsx:199-210
working_preferences: {
preferred_start_time: '09:00',
preferred_end_time: '17:00',
min_hours_per_week: 20,
max_hours_per_week: 40,
min_hours_per_day: 4,
max_hours_per_day: 12,
max_consecutive_days: 5,
min_days_off_between: 1,
weekend_availability: false,
overtime_willing: false,
preferred_shift_types: []
}
Tab 3: Skills & Certifications
Skill Entry Structure
// Source: src/components/staff/EnhancedStaffForm.tsx:81-93
skills_detailed: z.array(z.object({
skill_name: z.string(),
skill_category: z.string(),
proficiency_level: z.number().min(1).max(10).optional(),
certification_number: z.string().optional(),
certification_issuer: z.string().optional(),
last_training_date: z.string().optional(),
certification_expiry: z.string().optional(),
certification_required: z.boolean().optional(),
training_provider: z.string().optional(),
next_training_due: z.string().optional(),
is_active: z.boolean().optional()
})).optional()
Skill Fields
| Field | Type | Description |
|---|---|---|
| Skill Name | Text | Name of the skill |
| Category | Text | Skill category |
| Proficiency | Number 1-10 | Proficiency level |
| Cert Number | Text | Certification ID |
| Cert Issuer | Text | Issuing organisation |
| Last Training | Date | Most recent training |
| Cert Expiry | Date | When certification expires |
| Required | Boolean | If certification is required |
| Provider | Text | Training provider name |
| Next Due | Date | Next training date |
| Active | Boolean | If skill is current |
Tab 4: Availability
Availability Entry Structure
// Source: src/components/staff/EnhancedStaffForm.tsx:96-106
availability: z.array(z.object({
day_of_week: z.number().min(0).max(6),
start_time: z.string(),
end_time: z.string(),
is_available: z.boolean().optional(),
break_duration: z.string().optional(), // INTERVAL format: '00:30:00'
availability_pattern: z.string().optional(),
availability_priority: z.number().min(1).max(10).optional(),
time_slot_type: z.string().optional(),
availability_notes: z.string().optional()
})).optional()
Day of Week Values
| Value | Day |
|---|---|
| 0 | Sunday |
| 1 | Monday |
| 2 | Tuesday |
| 3 | Wednesday |
| 4 | Thursday |
| 5 | Friday |
| 6 | Saturday |
Update Process
Form Submission
// Source: src/components/staff/EnhancedStaffForm.tsx:364-388
const result = await updateStaffWithCompleteData(companyId, staff.id, {
basicInfo: {
id: staff.id,
company_id: companyId,
first_name: data.first_name,
last_name: data.last_name,
email: data.email,
phone: data.phone,
address: data.address,
date_of_birth: data.date_of_birth,
role_id: data.role_id,
employment_start_date: data.employment_start_date,
employment_end_date: data.employment_end_date ? data.employment_end_date : null,
notes: data.notes || null,
requires_clock_in: data.requires_clock_in,
contract_hours: data.contract_hours,
hourly_rate: data.hourly_rate ?? null,
pay_type: data.pay_type ?? null,
salary_amount: data.salary_amount ?? null,
employment_type: data.employment_type
},
working_preferences: data.working_preferences,
skills_detailed: data.skills_detailed,
availability: data.availability
})
Loading Existing Data
When editing, the form loads complete staff data:
// Source: src/components/staff/EnhancedStaffForm.tsx:256-262
async function loadEnhancedData() {
if (!staffData?.id || !companyId) return
try {
setLoadingEnhancedData(true)
const completeData = await getStaffWithFullDetails(staffData.id)
// ... populate form with complete data
}
}
API Endpoint
Update Staff
Endpoint: PUT /api/companies/[companyId]/staff/[staffId]
Request Body:
{
"basicInfo": {
"first_name": "John",
"last_name": "Smith",
"email": "john.smith@example.com",
"phone": "+44 7123 456789",
"role_id": "uuid",
"employment_start_date": "2024-01-15",
"contract_hours": 40,
"employment_type": "full-time"
},
"working_preferences": {
"preferred_start_time": "09:00",
"preferred_end_time": "17:00",
"max_hours_per_week": 40
},
"skills_detailed": [],
"availability": []
}
Response:
{
"success": true,
"data": {
"id": "staff-uuid",
"firstName": "John",
"lastName": "Smith",
"email": "john.smith@example.com",
"updatedAt": "2025-01-13T10:30:00Z"
}
}
Pay Type Conditional Fields
Field Visibility Logic
// Source: src/components/staff/EnhancedStaffForm.tsx:223-230
useEffect(() => {
if (watchPayType === 'hourly') {
// Hourly pay: clear salary_amount
setValue('salary_amount', undefined)
} else if (['weekly', 'monthly', 'annual'].includes(watchPayType || '')) {
// Salary pay: clear hourly_rate
setValue('hourly_rate', undefined)
}
}, [watchPayType, setValue])
Pay Type Options
| Pay Type | Shows Field | Format |
|---|---|---|
| Hourly | Hourly Rate | £/hour |
| Weekly | Salary Amount | £/week |
| Monthly | Salary Amount | £/month |
| Annual | Salary Amount | £/year |
Validation
Form Validation Schema
The form uses Zod validation:
// Source: src/components/staff/EnhancedStaffForm.tsx:32-107
const enhancedStaffSchema = z.object({
first_name: z.string().min(1, 'First name is required'),
last_name: z.string().min(1, 'Last name is required'),
email: z.string().email('Invalid email address'),
phone: z.string()
.regex(phoneRegex, 'Invalid phone number format')
.max(30, 'Phone number must not exceed 30 characters'),
address: z.string()
.min(1, 'Address is required')
.max(200, 'Address must not exceed 200 characters'),
// ... additional fields
})
Error Display
// Source: src/components/staff/EnhancedStaffForm.tsx:499-501
{errors.first_name && (
<p className="text-sm text-red-400 mt-1">{errors.first_name.message}</p>
)}
Error Handling
Submission Errors
// Source: src/components/staff/EnhancedStaffForm.tsx:430-432
} catch (err) {
console.error('Error saving staff:', err)
setError(err instanceof Error ? err.message : 'Failed to save staff member')
}
Error Display
// Source: src/components/staff/EnhancedStaffForm.tsx:464-467
{error && (
<Alert variant="destructive">
{error}
</Alert>
)}
Loading States
Role Loading
// Source: src/components/staff/EnhancedStaffForm.tsx:575-578
<option value="" disabled>
{loadingRoles ? 'Loading roles...' : 'Select a role...'}
</option>
Data Loading
// Source: src/components/staff/EnhancedStaffForm.tsx:470-474
{loadingEnhancedData && staff && (
<Alert>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Loading complete staff information...
</Alert>
)}
Best Practices
When Editing Staff
- Review current data - Check existing values before making changes
- Update one section at a time - Complete each tab before moving on
- Validate email changes - Ensure new email is correct (affects login)
- Check role permissions - Role changes affect system access
- Document changes in notes - Record significant updates
Data Integrity
- Never change staff ID - This is system-generated
- Preserve historical data - Use notes for change tracking
- Verify certifications - Check expiry dates when updating
- Update availability carefully - Affects scheduling suggestions
Related Documentation
- Adding Staff - Create new staff
- Staff Roles - Role management
- Deactivating Staff - Staff offboarding
Source Files:
src/components/staff/EnhancedStaffForm.tsx- Enhanced staff formsrc/components/staff/StaffDetail.tsx- Staff detail viewsrc/utils/api/staff.ts- Staff API utilitiessrc/lib/adapters/staff.adapter.ts- Staff data adapter