Skip to main content

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​

  1. Navigate to Dashboard → Scheduling
  2. Click on an empty time slot in a room row
  3. 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​

  1. Click the + Add Shift button in the calendar header
  2. 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: ''
})
FieldTypeRequiredDescription
Staff MemberSelect✅ YesStaff member to assign
RoomSelect✅ YesRoom/location for shift
DateDate✅ YesShift date (DD/MM/YYYY)
Start TimeTime✅ YesShift start (24-hour)
End TimeTime✅ YesShift end (24-hour)
Shift TypeSelect✅ YesType of shift
NotesText❌ NoOptional 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​

PropertyShown
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>
TypeDescriptionTypical Use
REGULARStandard working shiftDaily scheduled work
OVERTIMEAdditional hours beyond normalExtra coverage needs
ON_CALLAvailable if neededEmergency standby
EMERGENCYUrgent coverageLast-minute requirements
TRAININGTraining/developmentStaff development
MEETINGScheduled meetingsTeam 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​

RuleValidation
End after startEnd time must be after start time
Within operating hoursWarning if outside company hours
Minimum duration15 minutes minimum

Default Values​

FieldDefaultSource
Start timeClicked hour or 09:00Calendar click position
End timeStart + 8 hours or 17:00Calculated from start
DateCurrent calendar dateCalendar 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​

SeverityActionVisual
CRITICALBlocks submissionRed warning
HIGHWarning, can proceedOrange warning
MEDIUMInformationalYellow warning
LOWNotice onlyGrey 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​

  1. Form validation passes
  2. Conflicts checked (warnings shown if any)
  3. API call to create shift
  4. Modal closes on success
  5. 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​

FieldValidation
Staff memberMust be selected
RoomMust be selected
Start timeMust be valid time
End timeMust be after start time
Shift typeMust 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​

KeyAction
EnterSubmit form (when focused on submit button)
EscapeClose modal without saving
TabNavigate between fields

Best Practices​

  1. Check conflicts first - Review any warnings before submitting
  2. Use appropriate shift type - Helps with reporting and analytics
  3. Add notes for context - Useful for handovers and reference
  4. Verify times - Double-check 24-hour format


Source Files:

  • src/components/calendar/ShiftCreateModal.tsx - Shift creation form
  • src/contexts/SchedulingContext.tsx - Shift state management
  • src/services/schedulingService.ts - Scheduling business logic
  • src/app/api/scheduling/shifts/route.ts - API endpoint