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