Creating Shifts
This guide covers all methods for creating shifts in Shyfts, including the shift creation form, field requirements, and conflict handling.
Creating a Shift
Method 1: Click on Calendar
- Navigate to Dashboard → Scheduling
- Click on an empty time slot in a room row
- The shift creation modal opens with pre-filled defaults
// Source: src/components/calendar/RoomTimeGridCalendar.tsx:230-250
const handleSlotClick = (roomId: string, hour: number) => {
const startTime = `${hour.toString().padStart(2, '0')}:00`
const endTime = `${(hour + 1).toString().padStart(2, '0')}:00`
setCreateModalState({
isOpen: true,
defaultRoomId: roomId,
defaultDate: currentDate,
defaultTime: startTime
})
}
Method 2: Add Shift Button
- Click the + Add Shift button in the calendar header
- The shift creation modal opens without pre-filled values
Shift Creation Form
Form Fields
// Source: src/components/calendar/ShiftCreateModal.tsx:45-52
const [formData, setFormData] = useState({
staff_id: '',
room_id: normalizedDefaultRoom,
start_time: defaultTime || '09:00',
end_time: '17:00',
shift_type: 'REGULAR' as ShiftType,
notes: ''
})
| Field | Type | Required | Description |
|---|---|---|---|
| Staff Member | Select | ✅ Yes | Staff member to assign |
| Room | Select | ✅ Yes | Room/location for shift |
| Date | Date | ✅ Yes | Shift date (DD/MM/YYYY) |
| Start Time | Time | ✅ Yes | Shift start (24-hour) |
| End Time | Time | ✅ Yes | Shift end (24-hour) |
| Shift Type | Select | ✅ Yes | Type of shift |
| Notes | Text | ❌ No | Optional notes |
Staff Selection
Active Staff List
Only active staff members appear in the selection dropdown:
// Source: src/components/calendar/ShiftCreateModal.tsx:98-115
const { data: staffData } = await supabase
.from('staff')
.select(`
id,
first_name,
last_name,
staff_roles (
id,
name,
color
)
`)
.eq('company_id', companyId)
.eq('is_active', true)
.order('first_name')
Staff Display Format
┌─────────────────────────────────────┐
│ Staff Member ▼ │
├─────────────────────────────────────┤
│ Select staff member... │
│ ● John Smith (Nurse) │
│ ● Sarah Johnson (Doctor) │
│ ● Mike Brown (Receptionist) │
└─────────────────────────────────────┘
Room Selection
Active Rooms
Rooms are filtered by active status:
// Source: src/components/calendar/ShiftCreateModal.tsx:117-130
const { data: roomsData } = await supabase
.from('rooms')
.select(`
id,
name,
room_types (
id,
name
)
`)
.eq('company_id', companyId)
.eq('is_active', true)
.order('name')
Room Display
| Property | Shown |
|---|---|
| Room name | ✅ Yes |
| Room type | ✅ Yes (in parentheses) |
| Capacity status | ❌ No (shown via conflicts) |
Shift Types
Available Types
// Source: src/components/calendar/ShiftCreateModal.tsx:376-383
<select>
<option value="REGULAR">Regular</option>
<option value="OVERTIME">Overtime</option>
<option value="ON_CALL">On Call</option>
<option value="EMERGENCY">Emergency</option>
<option value="TRAINING">Training</option>
<option value="MEETING">Meeting</option>
</select>
| Type | Description | Typical Use |
|---|---|---|
| REGULAR | Standard working shift | Daily scheduled work |
| OVERTIME | Additional hours beyond normal | Extra coverage needs |
| ON_CALL | Available if needed | Emergency standby |
| EMERGENCY | Urgent coverage | Last-minute requirements |
| TRAINING | Training/development | Staff development |
| MEETING | Scheduled meetings | Team meetings, reviews |
Time Selection
Time Format
All times use 24-hour format (HH:mm):
// Source: src/components/calendar/ShiftCreateModal.tsx:340-365
<input
type="time"
value={formData.start_time}
onChange={(e) => setFormData(prev => ({
...prev,
start_time: e.target.value
}))}
className="form-input"
/>
Time Validation
| Rule | Validation |
|---|---|
| End after start | End time must be after start time |
| Within operating hours | Warning if outside company hours |
| Minimum duration | 15 minutes minimum |
Default Values
| Field | Default | Source |
|---|---|---|
| Start time | Clicked hour or 09:00 | Calendar click position |
| End time | Start + 8 hours or 17:00 | Calculated from start |
| Date | Current calendar date | Calendar state |
Conflict Detection
Real-Time Checking
Conflicts are checked as you fill the form:
// Source: src/components/calendar/ShiftCreateModal.tsx:180-210
useEffect(() => {
const checkConflicts = async () => {
if (!formData.staff_id || !formData.room_id) return
const conflicts = await schedulingService.detectConflicts({
staff_id: formData.staff_id,
room_id: formData.room_id,
date: selectedDate,
start_time: formData.start_time,
end_time: formData.end_time
})
setDetectedConflicts(conflicts)
}
checkConflicts()
}, [formData.staff_id, formData.room_id, formData.start_time, formData.end_time])
Conflict Display
// Source: src/components/calendar/ShiftCreateModal.tsx:425-455
{detectedConflicts && detectedConflicts.hasConflicts && (
<div className="mt-4 p-4 rounded-lg bg-red-500/10 border border-red-500/20">
<div className="flex items-center gap-2 text-red-400 font-medium">
<AlertTriangle className="h-5 w-5" />
Scheduling Conflicts Detected
</div>
<ul className="mt-2 space-y-1 text-sm text-red-300">
{detectedConflicts.conflicts.map((conflict, index) => (
<li key={index}>• {conflict.message}</li>
))}
</ul>
</div>
)}
Conflict Severity
| Severity | Action | Visual |
|---|---|---|
| CRITICAL | Blocks submission | Red warning |
| HIGH | Warning, can proceed | Orange warning |
| MEDIUM | Informational | Yellow warning |
| LOW | Notice only | Grey notice |
For detailed conflict information, see Conflict Resolution.
Form Submission
Submit Handler
// Source: src/components/calendar/ShiftCreateModal.tsx:250-300
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
try {
// Combine date and time into full timestamp
const startDateTime = combineUKDateAndTimeToUTC(
selectedDate,
formData.start_time
)
const endDateTime = combineUKDateAndTimeToUTC(
selectedDate,
formData.end_time
)
const result = await createShift({
staff_id: formData.staff_id,
room_id: formData.room_id,
start_time: startDateTime,
end_time: endDateTime,
shift_type: formData.shift_type,
notes: formData.notes || null
})
if (result) {
onClose()
refreshShifts()
}
} catch (error) {
setError('Failed to create shift')
} finally {
setIsSubmitting(false)
}
}
Success Flow
- Form validation passes
- Conflicts checked (warnings shown if any)
- API call to create shift
- Modal closes on success
- Calendar refreshes to show new shift
Error Handling
// Source: src/components/calendar/ShiftCreateModal.tsx:465-475
{error && (
<div className="p-4 rounded-lg bg-red-500/10 border border-red-500/20">
<p className="text-red-400 text-sm">{error}</p>
</div>
)}
API Endpoint
Create Shift
Endpoint: POST /api/scheduling/shifts
Request Body:
{
"staff_id": "uuid",
"room_id": "uuid",
"start_time": "2025-01-13T09:00:00Z",
"end_time": "2025-01-13T17:00:00Z",
"shift_type": "REGULAR",
"notes": "Optional notes"
}
Response:
{
"success": true,
"data": {
"id": "uuid",
"staff_id": "uuid",
"room_id": "uuid",
"start_time": "2025-01-13T09:00:00Z",
"end_time": "2025-01-13T17:00:00Z",
"shift_type": "REGULAR",
"status": "SCHEDULED",
"created_at": "2025-01-13T08:30:00Z"
}
}
Context Integration
SchedulingContext
Shift creation uses the SchedulingContext:
// Source: src/contexts/SchedulingContext.tsx:85-120
const createShift = async (data: CreateShiftData): Promise<boolean> => {
try {
setLoading(true)
const response = await fetch('/api/scheduling/shifts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...data,
company_id: companyId
})
})
if (!response.ok) {
throw new Error('Failed to create shift')
}
await refreshShifts()
return true
} catch (error) {
console.error('Create shift error:', error)
return false
} finally {
setLoading(false)
}
}
Form Validation
Required Fields
| Field | Validation |
|---|---|
| Staff member | Must be selected |
| Room | Must be selected |
| Start time | Must be valid time |
| End time | Must be after start time |
| Shift type | Must be valid type |
Time Validation
// Source: src/components/calendar/ShiftCreateModal.tsx:230-245
const validateTimes = () => {
const start = moment(formData.start_time, 'HH:mm')
const end = moment(formData.end_time, 'HH:mm')
if (end.isSameOrBefore(start)) {
setError('End time must be after start time')
return false
}
return true
}
Quick Creation Tips
Keyboard Shortcuts
| Key | Action |
|---|---|
Enter | Submit form (when focused on submit button) |
Escape | Close modal without saving |
Tab | Navigate between fields |
Best Practices
- Check conflicts first - Review any warnings before submitting
- Use appropriate shift type - Helps with reporting and analytics
- Add notes for context - Useful for handovers and reference
- Verify times - Double-check 24-hour format
Related Documentation
- Calendar Overview - Calendar interface guide
- Editing Shifts - Modify existing shifts
- Conflict Resolution - Handle conflicts
- Bulk Operations - Create multiple shifts
Source Files:
src/components/calendar/ShiftCreateModal.tsx- Shift creation formsrc/contexts/SchedulingContext.tsx- Shift state managementsrc/services/schedulingService.ts- Scheduling business logicsrc/app/api/scheduling/shifts/route.ts- API endpoint