Skip to main content

Conflict Resolution

Shyfts includes an advanced conflict detection system that identifies scheduling issues before they become problems. This guide covers all conflict types, severity levels, and resolution strategies.


Conflict Detection Overview

How It Works

Conflicts are automatically detected when creating or editing shifts:

// Source: src/services/schedulingService.ts:156-180
static async detectConflicts(params: ConflictCheckParams): Promise<ConflictDetection> {
const conflicts: Conflict[] = []
const warnings: Warning[] = []

// Run all conflict checks
const [
overlappingShifts,
leaveConflicts,
constraintViolations,
roomCapacity,
hoursExceeded
] = await Promise.all([
this.checkOverlappingShifts(params),
this.checkLeaveConflicts(params),
this.checkConstraintViolations(params),
this.checkRoomCapacity(params),
this.checkMaxHoursExceeded(params)
])

// Compile results
return {
hasConflicts: conflicts.length > 0,
conflicts,
warnings,
canProceed: !conflicts.some(c => c.severity === 'CRITICAL')
}
}

When Conflicts Are Checked

EventChecked
Creating a new shift✅ Yes
Editing an existing shift✅ Yes
Moving a shift (drag-drop)✅ Yes
Bulk shift creation✅ Yes (per shift)
Staff assignment change✅ Yes

Conflict Types

1. Overlapping Shift (CRITICAL)

Staff member is already scheduled during the requested time.

// Source: src/services/schedulingService.ts:183-210
const checkOverlappingShifts = async (params: ConflictCheckParams) => {
const { data: overlaps } = await supabase
.from('shifts')
.select('id, start_time, end_time, room_id')
.eq('staff_id', params.staff_id)
.neq('id', params.exclude_id || '')
.gte('end_time', params.start_time)
.lte('start_time', params.end_time)

if (overlaps && overlaps.length > 0) {
return {
type: 'OVERLAPPING_SHIFT',
severity: 'CRITICAL',
message: `Staff member has ${overlaps.length} overlapping shift(s)`,
details: overlaps.map(s => ({
shift_id: s.id,
time: `${formatTime(s.start_time)} - ${formatTime(s.end_time)}`
}))
}
}
return null
}

Visual Indicator: Red warning with block icon

Resolution Options:

  • Adjust shift times to avoid overlap
  • Assign a different staff member
  • Cancel or reschedule the conflicting shift

2. Staff on Approved Leave (CRITICAL)

Staff member has approved leave during the requested shift time.

// Source: src/services/schedulingService.ts:212-245
const checkLeaveConflicts = async (params: ConflictCheckParams) => {
const shiftDate = moment(params.start_time).format('YYYY-MM-DD')

const { data: leaveRequests } = await supabase
.from('leave_requests')
.select('id, leave_type_id, start_date, end_date, status')
.eq('staff_id', params.staff_id)
.eq('status', 'APPROVED')
.lte('start_date', shiftDate)
.gte('end_date', shiftDate)

if (leaveRequests && leaveRequests.length > 0) {
return {
type: 'STAFF_ON_APPROVED_LEAVE',
severity: 'CRITICAL',
message: 'Staff member is on approved leave for this date',
details: {
leave_type: leaveRequests[0].leave_type_id,
dates: `${leaveRequests[0].start_date} - ${leaveRequests[0].end_date}`
}
}
}
return null
}

Visual Indicator: Red warning with calendar-off icon

Resolution Options:

  • Choose a different date
  • Assign a different staff member
  • Request leave modification (if appropriate)

3. Constraint Violation (CRITICAL/MEDIUM)

Staff member's working preferences or constraints are violated.

// Source: src/services/schedulingService.ts:256-290
const checkConstraintViolations = async (params: ConflictCheckParams) => {
const { data: preferences } = await supabase
.from('staff_preferences')
.select('*')
.eq('staff_id', params.staff_id)
.single()

const violations: Violation[] = []
const shiftDay = moment(params.start_time).day()
const shiftStartHour = moment(params.start_time).hour()

// Check unavailable days
if (preferences?.unavailable_days?.includes(shiftDay)) {
violations.push({
type: 'UNAVAILABLE_DAY',
severity: 'CRITICAL',
message: `Staff is unavailable on ${getDayName(shiftDay)}`
})
}

// Check preferred working hours
if (preferences?.preferred_start_hour && shiftStartHour < preferences.preferred_start_hour) {
violations.push({
type: 'OUTSIDE_PREFERRED_HOURS',
severity: 'MEDIUM',
message: `Shift starts before preferred time (${preferences.preferred_start_hour}:00)`
})
}

return violations
}

Constraint Types:

ConstraintSeverityDescription
Unavailable daysCRITICALStaff cannot work on specific days
Maximum hours per dayHIGHExceeds daily hour limit
Preferred hoursMEDIUMOutside preferred working window
Minimum rest periodHIGHInsufficient rest between shifts

4. Room Capacity Exceeded (CRITICAL)

Room already has maximum allowed concurrent shifts.

// Source: src/services/schedulingService.ts:292-330
const checkRoomCapacity = async (params: ConflictCheckParams) => {
// Get room capacity
const { data: room } = await supabase
.from('rooms')
.select('id, name, capacity')
.eq('id', params.room_id)
.single()

// Count existing shifts in the time window
const { count } = await supabase
.from('shifts')
.select('*', { count: 'exact', head: true })
.eq('room_id', params.room_id)
.neq('id', params.exclude_id || '')
.gte('end_time', params.start_time)
.lte('start_time', params.end_time)

if (count !== null && room?.capacity && count >= room.capacity) {
return {
type: 'ROOM_CAPACITY_EXCEEDED',
severity: 'CRITICAL',
message: `Room "${room.name}" is at full capacity (${room.capacity})`,
details: {
current_bookings: count,
max_capacity: room.capacity
}
}
}
return null
}

Visual Indicator: Red warning with building icon

Resolution Options:

  • Choose a different room
  • Adjust the time to avoid overlap
  • Move existing shifts to other rooms

5. Room Partially Booked (LOW)

Room has some bookings but is not at capacity.

// Source: src/services/schedulingService.ts:332-350
// This is informational, not blocking
if (count !== null && room?.capacity && count > 0 && count < room.capacity) {
return {
type: 'ROOM_PARTIALLY_BOOKED',
severity: 'LOW',
message: `Room has ${count} of ${room.capacity} slots filled`,
details: {
current_bookings: count,
remaining_capacity: room.capacity - count
}
}
}

Visual Indicator: Grey notice

Action: Informational only, no action required

6. Maximum Hours Exceeded (HIGH)

Staff member would exceed their weekly hour limit.

// Source: src/services/schedulingService.ts:352-395
const checkMaxHoursExceeded = async (params: ConflictCheckParams) => {
// Get staff max hours setting
const { data: staff } = await supabase
.from('staff')
.select('max_hours_per_week')
.eq('id', params.staff_id)
.single()

if (!staff?.max_hours_per_week) return null

// Calculate week start/end
const weekStart = moment(params.start_time).startOf('isoWeek')
const weekEnd = moment(params.start_time).endOf('isoWeek')

// Sum existing hours
const { data: weekShifts } = await supabase
.from('shifts')
.select('start_time, end_time')
.eq('staff_id', params.staff_id)
.gte('start_time', weekStart.toISOString())
.lte('end_time', weekEnd.toISOString())

const existingHours = weekShifts?.reduce((sum, s) => {
return sum + moment(s.end_time).diff(moment(s.start_time), 'hours', true)
}, 0) || 0

const newShiftHours = moment(params.end_time).diff(moment(params.start_time), 'hours', true)

if (existingHours + newShiftHours > staff.max_hours_per_week) {
return {
type: 'MAX_HOURS_EXCEEDED',
severity: 'HIGH',
message: `Would exceed weekly limit (${staff.max_hours_per_week}h)`,
details: {
current_hours: existingHours.toFixed(1),
new_shift_hours: newShiftHours.toFixed(1),
total_would_be: (existingHours + newShiftHours).toFixed(1),
max_allowed: staff.max_hours_per_week
}
}
}
return null
}

Visual Indicator: Orange warning with clock icon

Resolution Options:

  • Reduce shift duration
  • Assign a different staff member
  • Review and adjust other shifts in the week

Severity Levels

CRITICAL (Red)

Action: Blocks shift creation

VisualDescription
Red backgroundbg-red-500/10
Red borderborder-red-500/20
Red texttext-red-400
IconAlertTriangle
// Source: src/components/calendar/ConflictIndicator.tsx:17-24
const getIndicatorColor = () => {
const criticalConflicts = conflicts.filter(c => c.severity === 'CRITICAL')
if (criticalConflicts.length > 0) {
return 'text-red-400 bg-red-500/10 border-red-500/20'
}
// ...
}

HIGH (Orange)

Action: Warning, allows proceed with confirmation

VisualDescription
Orange backgroundbg-orange-500/10
Orange borderborder-orange-500/20
Orange texttext-orange-400
IconAlertCircle

MEDIUM (Yellow)

Action: Informational warning

VisualDescription
Yellow backgroundbg-yellow-500/10
Yellow borderborder-yellow-500/20
Yellow texttext-yellow-400
IconInfo

LOW (Grey)

Action: Notice only, no action needed

VisualDescription
Grey backgroundbg-white/5
Grey borderborder-white/10
Grey texttext-secondary-text
IconInfo

Conflict Indicator Component

Visual Display

// Source: src/components/calendar/ConflictIndicator.tsx:35-75
export function ConflictIndicator({ conflicts }: ConflictIndicatorProps) {
if (!conflicts || conflicts.length === 0) return null

const criticalConflicts = conflicts.filter(c => c.severity === 'CRITICAL')
const highConflicts = conflicts.filter(c => c.severity === 'HIGH')
const otherConflicts = conflicts.filter(c =>
c.severity === 'MEDIUM' || c.severity === 'LOW'
)

return (
<div className="space-y-2">
{/* Critical conflicts */}
{criticalConflicts.length > 0 && (
<div className="p-3 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-4 w-4" />
<span>{criticalConflicts.length} Critical Issue(s)</span>
</div>
<ul className="mt-2 space-y-1 text-sm text-red-300">
{criticalConflicts.map((c, i) => (
<li key={i}>{c.message}</li>
))}
</ul>
</div>
)}

{/* High severity warnings */}
{highConflicts.length > 0 && (
<div className="p-3 rounded-lg bg-orange-500/10 border border-orange-500/20">
{/* ... */}
</div>
)}

{/* Other notices */}
{otherConflicts.length > 0 && (
<div className="p-3 rounded-lg bg-white/5 border border-white/10">
{/* ... */}
</div>
)}
</div>
)
}

Resolution Suggestions

Automatic Suggestions

The system generates resolution suggestions for conflicts:

// Source: src/services/schedulingService.ts:683-730
private static generateResolutions(conflicts: Conflict[]): ConflictResolution[] {
const resolutions: ConflictResolution[] = []

conflicts.forEach(conflict => {
switch (conflict.type) {
case 'OVERLAPPING_SHIFT':
resolutions.push({
type: 'ADJUST_TIME',
description: 'Adjust shift time to avoid overlap',
action: 'modify_time'
})
resolutions.push({
type: 'CHANGE_STAFF',
description: 'Assign a different staff member',
action: 'change_assignment'
})
break

case 'ROOM_CAPACITY_EXCEEDED':
resolutions.push({
type: 'CHANGE_ROOM',
description: 'Select a different room',
action: 'modify_room'
})
break

case 'MAX_HOURS_EXCEEDED':
resolutions.push({
type: 'REDUCE_DURATION',
description: 'Shorten shift duration',
action: 'modify_time'
})
resolutions.push({
type: 'CHANGE_STAFF',
description: 'Assign staff with available hours',
action: 'change_assignment'
})
break
}
})

return resolutions
}

Resolution Types

ResolutionDescriptionApplies To
ADJUST_TIMEChange start/end timesOverlaps, constraints
CHANGE_STAFFAssign different personAll staff-related
CHANGE_ROOMSelect different roomRoom conflicts
REDUCE_DURATIONShorten shift lengthHours exceeded
RESCHEDULEMove to different dateLeave conflicts

Conflict Workflow

Step-by-Step Process

1. User creates/edits shift

2. Conflict detection runs

3. Results displayed

┌─────────────────────────────┐
│ No conflicts? │
│ → Proceed with creation │
└─────────────────────────────┘

┌─────────────────────────────┐
│ CRITICAL conflicts? │
│ → Block + show resolution │
└─────────────────────────────┘

┌─────────────────────────────┐
│ HIGH/MEDIUM warnings? │
│ → Show warning │
│ → Allow proceed with │
│ acknowledgment │
└─────────────────────────────┘

4. User resolves or acknowledges

5. Shift created/updated

Conflict Modal

// Source: src/components/calendar/ShiftCreateModal.tsx:425-480
{detectedConflicts && detectedConflicts.hasConflicts && (
<div className="mt-4">
<ConflictIndicator conflicts={detectedConflicts.conflicts} />

{/* Show resolutions for critical conflicts */}
{detectedConflicts.conflicts.some(c => c.severity === 'CRITICAL') && (
<div className="mt-4 p-4 rounded-lg bg-white/5">
<h4 className="text-sm font-medium text-primary-text mb-2">
Suggested Resolutions
</h4>
<ul className="space-y-2 text-sm text-secondary-text">
{detectedConflicts.resolutions?.map((r, i) => (
<li key={i} className="flex items-center gap-2">
<ArrowRight className="h-3 w-3" />
{r.description}
</li>
))}
</ul>
</div>
)}

{/* Acknowledge checkbox for non-critical */}
{!detectedConflicts.conflicts.some(c => c.severity === 'CRITICAL') && (
<label className="flex items-center gap-2 mt-4 text-sm text-secondary-text">
<input
type="checkbox"
checked={acknowledgedWarnings}
onChange={(e) => setAcknowledgedWarnings(e.target.checked)}
className="form-checkbox"
/>
I acknowledge these warnings and wish to proceed
</label>
)}
</div>
)}

Best Practices

Preventing Conflicts

  1. Review staff availability - Check constraints before scheduling
  2. Monitor room capacity - Keep track of room utilisation
  3. Track weekly hours - Monitor staff hours early in the week
  4. Plan around leave - Check approved leave before scheduling

Handling Conflicts

  1. Address CRITICAL first - These must be resolved
  2. Review HIGH warnings - Consider impact before proceeding
  3. Document decisions - Add notes when overriding warnings
  4. Communicate changes - Notify affected staff

Common Scenarios

ScenarioBest Resolution
Staff double-bookedAssign different staff member
Room at capacityMove to alternative room
Hours limit reachedSplit shift between staff
Staff on leaveReschedule or reassign

API Response Structure

Conflict Detection Response

{
"hasConflicts": true,
"conflicts": [
{
"type": "OVERLAPPING_SHIFT",
"severity": "CRITICAL",
"message": "Staff member has 1 overlapping shift(s)",
"details": {
"shift_id": "uuid",
"time": "09:00 - 17:00"
}
}
],
"warnings": [
{
"type": "OUTSIDE_PREFERRED_HOURS",
"severity": "MEDIUM",
"message": "Shift starts before preferred time (10:00)"
}
],
"resolutions": [
{
"type": "ADJUST_TIME",
"description": "Adjust shift time to avoid overlap",
"action": "modify_time"
}
],
"canProceed": false
}


Source Files:

  • src/services/schedulingService.ts - Conflict detection logic
  • src/components/calendar/ConflictIndicator.tsx - Visual display
  • src/components/calendar/ShiftCreateModal.tsx - Form integration
  • src/types/scheduling.ts - Type definitions