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
| Field | Display Format |
|---|---|
| Staff Name | First Name Last Name |
| Role | Staff role with colour indicator |
| Room | Room name |
| Date | DD/MM/YYYY |
| Time | HH:mm - HH:mm (24-hour) |
| Shift Type | Type badge |
| Notes | Full text if present |
| Status | SCHEDULED, COMPLETED, CANCELLED |
Editing Methods
Method 1: Edit via Modal
- Click on a shift block
- Click the Edit button in the details modal
- The shift edit form opens with current values
- Modify required fields
- 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
| Action | Result |
|---|---|
| Drag to different time | Updates start/end time, preserves duration |
| Drag to different room | Changes room assignment |
| Drag to different room + time | Updates 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
}
})
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:
| Field | Editable | Notes |
|---|---|---|
| Staff Member | ✅ Yes | Change assignment |
| Room | ✅ Yes | Change location |
| Date | ✅ Yes | Reschedule to different day |
| Start Time | ✅ Yes | Adjust timing |
| End Time | ✅ Yes | Adjust duration |
| Shift Type | ✅ Yes | Change category |
| Notes | ✅ Yes | Update 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
- Click on the shift to view details
- Click the Delete button
- 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
| Check | Behaviour |
|---|---|
| Overlapping shifts | Excludes current shift from comparison |
| Room capacity | Recalculates with updated times |
| Leave conflicts | Checks against approved leave |
| Constraint violations | Validates against staff preferences |
Shift History
Tracking Changes
Shift modifications are tracked in the database:
| Field | Description |
|---|---|
created_at | Original creation timestamp |
updated_at | Last modification timestamp |
created_by | User who created the shift |
updated_by | User who last modified |
Status Changes
Available Statuses
| Status | Description | Editable |
|---|---|---|
| SCHEDULED | Upcoming shift | ✅ All fields |
| IN_PROGRESS | Currently active | ⚠️ Limited fields |
| COMPLETED | Past shift | ❌ Read-only |
| CANCELLED | Cancelled 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
| Key | Action |
|---|---|
Escape | Close modal without saving |
Enter | Save changes (when focused on save button) |
Tab | Navigate between fields |
Best Practices
- Check conflicts after changes - Ensure no new conflicts are introduced
- Update notes when rescheduling - Document reason for changes
- Notify affected staff - Changes may trigger notifications
- Review before saving - Verify all changes are correct
Quick Reschedule
To quickly move a shift to a different time:
- Click and hold the shift (200ms)
- Drag to the new time slot
- Release to confirm
Dragging a shift will immediately update it without a confirmation dialog. Use the edit modal for more careful changes.
Related Documentation
- Calendar Overview - Calendar interface guide
- Creating Shifts - Add new shifts
- Conflict Resolution - Handle conflicts
- Bulk Operations - Manage multiple shifts
Source Files:
src/components/calendar/ShiftDetailModal.tsx- Shift details viewsrc/components/calendar/ShiftCreateModal.tsx- Edit form (shared with create)src/components/calendar/RoomTimeGridCalendar.tsx- Drag-and-drop handlingsrc/contexts/SchedulingContext.tsx- Update/delete functionssrc/services/schedulingService.ts- Business logic