Skip to main content

Editing Shifts

This guide covers all methods for modifying existing shifts, including editing via modal, drag-and-drop repositioning, and deletion.


Viewing Shift Details

Click to View

Click on any shift block in the calendar to view its details:

// Source: src/components/calendar/ShiftDetailModal.tsx:25-50
interface ShiftDetailModalProps {
shift: Shift | null
isOpen: boolean
onClose: () => void
onEdit: () => void
onDelete: () => void
}

export function ShiftDetailModal({
shift,
isOpen,
onClose,
onEdit,
onDelete
}: ShiftDetailModalProps) {
if (!shift) return null

return (
<Modal isOpen={isOpen} onClose={onClose}>
<div className="p-6">
<h2 className="text-xl font-semibold text-primary-text">
Shift Details
</h2>
{/* Shift information display */}
</div>
</Modal>
)
}

Details Displayed

FieldDisplay Format
Staff NameFirst Name Last Name
RoleStaff role with colour indicator
RoomRoom name
DateDD/MM/YYYY
TimeHH:mm - HH:mm (24-hour)
Shift TypeType badge
NotesFull text if present
StatusSCHEDULED, COMPLETED, CANCELLED

Editing Methods

Method 1: Edit via Modal

  1. Click on a shift block
  2. Click the Edit button in the details modal
  3. The shift edit form opens with current values
  4. Modify required fields
  5. Click Save Changes
// Source: src/components/calendar/ShiftDetailModal.tsx:75-90
<div className="flex gap-3 mt-6">
<button
onClick={onEdit}
className="form-button primary flex-1"
>
<Edit2 className="h-4 w-4 mr-2" />
Edit
</button>
<button
onClick={onDelete}
className="form-button secondary text-red-400"
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</button>
</div>

Method 2: Drag and Drop

Drag shift blocks to new positions to change time or room:

// Source: src/components/calendar/RoomTimeGridCalendar.tsx:252-285
const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event

if (!over || active.id === over.id) return

const shiftId = active.id as string
const [targetRoomId, targetHour] = (over.id as string).split('-')

try {
await updateShiftPosition(shiftId, {
room_id: targetRoomId,
start_time: calculateNewStartTime(targetHour),
end_time: calculateNewEndTime(targetHour, shift.duration)
})

refreshShifts()
} catch (error) {
console.error('Failed to move shift:', error)
}
}

Drag Behaviour

ActionResult
Drag to different timeUpdates start/end time, preserves duration
Drag to different roomChanges room assignment
Drag to different room + timeUpdates both room and time

Drag Sensor Configuration

// Source: src/components/calendar/RoomTimeGridCalendar.tsx:77-93
const mouseSensor = useSensor(MouseSensor, {
activationConstraint: {
delay: 200, // 200ms hold before drag starts
tolerance: 5 // 5px movement tolerance
}
})

const touchSensor = useSensor(TouchSensor, {
activationConstraint: {
delay: 200,
tolerance: 5
}
})
Click vs Drag

The 200ms delay allows the system to distinguish between clicking (to view/edit) and dragging (to move). Hold for 200ms before moving to initiate a drag.


Edit Form

Editable Fields

All fields from the creation form can be edited:

FieldEditableNotes
Staff Member✅ YesChange assignment
Room✅ YesChange location
Date✅ YesReschedule to different day
Start Time✅ YesAdjust timing
End Time✅ YesAdjust duration
Shift Type✅ YesChange category
Notes✅ YesUpdate information

Edit Mode Detection

// Source: src/components/calendar/ShiftCreateModal.tsx:55-75
// Modal handles both create and edit modes
const isEditMode = !!existingShift

useEffect(() => {
if (isEditMode && existingShift) {
setFormData({
staff_id: existingShift.staff_id,
room_id: existingShift.room_id,
start_time: moment(existingShift.start_time).format('HH:mm'),
end_time: moment(existingShift.end_time).format('HH:mm'),
shift_type: existingShift.shift_type,
notes: existingShift.notes || ''
})
}
}, [existingShift])

Update API

Update Endpoint

Endpoint: PUT /api/scheduling/shifts/[id]

Request Body:

{
"staff_id": "uuid",
"room_id": "uuid",
"start_time": "2025-01-13T10:00:00Z",
"end_time": "2025-01-13T18:00:00Z",
"shift_type": "REGULAR",
"notes": "Updated notes"
}

Response:

{
"success": true,
"data": {
"id": "shift-uuid",
"staff_id": "uuid",
"room_id": "uuid",
"start_time": "2025-01-13T10:00:00Z",
"end_time": "2025-01-13T18:00:00Z",
"shift_type": "REGULAR",
"notes": "Updated notes",
"updated_at": "2025-01-13T09:30:00Z"
}
}

Move Endpoint (Drag and Drop)

Endpoint: PATCH /api/scheduling/shifts/move

Request Body:

{
"shift_id": "uuid",
"room_id": "new-room-uuid",
"start_time": "2025-01-13T11:00:00Z",
"end_time": "2025-01-13T19:00:00Z"
}

Context Integration

Update Function

// Source: src/contexts/SchedulingContext.tsx:122-155
const updateShift = async (
id: string,
updates: Partial<CreateShiftData>
): Promise<boolean> => {
try {
setLoading(true)

const response = await fetch(`/api/scheduling/shifts/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
})

if (!response.ok) {
throw new Error('Failed to update shift')
}

await refreshShifts()
return true
} catch (error) {
console.error('Update shift error:', error)
return false
} finally {
setLoading(false)
}
}

Deleting Shifts

Single Shift Deletion

  1. Click on the shift to view details
  2. Click the Delete button
  3. Confirm in the confirmation dialog
// Source: src/contexts/SchedulingContext.tsx:157-185
const deleteShift = async (id: string): Promise<void> => {
try {
setLoading(true)

const response = await fetch(`/api/scheduling/shifts/${id}`, {
method: 'DELETE'
})

if (!response.ok) {
throw new Error('Failed to delete shift')
}

await refreshShifts()
} catch (error) {
console.error('Delete shift error:', error)
throw error
} finally {
setLoading(false)
}
}

Confirmation Dialog

// Source: src/components/calendar/ShiftDetailModal.tsx:92-110
const handleDelete = async () => {
if (window.confirm('Are you sure you want to delete this shift?')) {
try {
await onDelete()
onClose()
} catch (error) {
setError('Failed to delete shift')
}
}
}

Bulk Deletion

For deleting multiple shifts, see Bulk Operations.


Conflict Checking on Edit

Re-validation

When editing a shift, conflicts are re-checked:

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

// Exclude the current shift from overlap checks when editing
const excludeShiftId = params.shift_id || null

// Check for overlapping shifts (excluding current shift)
const overlaps = await this.checkOverlappingShifts({
...params,
exclude_id: excludeShiftId
})

if (overlaps.length > 0) {
conflicts.push({
type: 'OVERLAPPING_SHIFT',
severity: 'CRITICAL',
message: `Staff member already has ${overlaps.length} overlapping shift(s)`
})
}

// Continue with other conflict checks...
return { hasConflicts: conflicts.length > 0, conflicts }
}

Edit-Specific Validation

CheckBehaviour
Overlapping shiftsExcludes current shift from comparison
Room capacityRecalculates with updated times
Leave conflictsChecks against approved leave
Constraint violationsValidates against staff preferences

Shift History

Tracking Changes

Shift modifications are tracked in the database:

FieldDescription
created_atOriginal creation timestamp
updated_atLast modification timestamp
created_byUser who created the shift
updated_byUser who last modified

Status Changes

Available Statuses

StatusDescriptionEditable
SCHEDULEDUpcoming shift✅ All fields
IN_PROGRESSCurrently active⚠️ Limited fields
COMPLETEDPast shift❌ Read-only
CANCELLEDCancelled shift❌ Read-only

Status Transitions

SCHEDULED → IN_PROGRESS → COMPLETED

CANCELLED

Editing Restrictions by Status

// Source: src/services/schedulingService.ts:480-495
static canEditShift(shift: Shift): { canEdit: boolean; reason?: string } {
if (shift.status === 'COMPLETED') {
return { canEdit: false, reason: 'Completed shifts cannot be edited' }
}

if (shift.status === 'CANCELLED') {
return { canEdit: false, reason: 'Cancelled shifts cannot be edited' }
}

if (shift.status === 'IN_PROGRESS') {
return { canEdit: true, reason: 'Limited editing available for active shifts' }
}

return { canEdit: true }
}

Quick Edit Tips

Keyboard Shortcuts

KeyAction
EscapeClose modal without saving
EnterSave changes (when focused on save button)
TabNavigate between fields

Best Practices

  1. Check conflicts after changes - Ensure no new conflicts are introduced
  2. Update notes when rescheduling - Document reason for changes
  3. Notify affected staff - Changes may trigger notifications
  4. Review before saving - Verify all changes are correct

Quick Reschedule

To quickly move a shift to a different time:

  1. Click and hold the shift (200ms)
  2. Drag to the new time slot
  3. Release to confirm
warning

Dragging a shift will immediately update it without a confirmation dialog. Use the edit modal for more careful changes.



Source Files:

  • src/components/calendar/ShiftDetailModal.tsx - Shift details view
  • src/components/calendar/ShiftCreateModal.tsx - Edit form (shared with create)
  • src/components/calendar/RoomTimeGridCalendar.tsx - Drag-and-drop handling
  • src/contexts/SchedulingContext.tsx - Update/delete functions
  • src/services/schedulingService.ts - Business logic