Skip to main content

Shift Trading

This guide covers how to request shift swaps with colleagues, manage swap requests, and understand the approval process.


Overview

What is Shift Trading?

Shift trading allows staff to exchange shifts with colleagues:

  1. Request Swap - Ask a colleague to swap shifts
  2. Accept/Decline - Colleague responds to request
  3. Manager Approval - Manager reviews and approves
  4. Shift Exchange - Shifts are reassigned

Swap Workflow

Request Swap → Colleague Reviews → Colleague Accepts/Declines
↓ (if accepted)
Manager Approval → Shifts Exchanged

Creating a Swap Request

Prerequisites

Before requesting a swap:

RequirementDescription
Future ShiftCannot swap past shifts
Confirmed StatusShift must be confirmed
Same CompanyColleague must be in same company
Available ColleagueTarget must have a shift to swap

Swap Request API

Endpoint: POST /api/staff/shifts/swap-requests

// Source: src/app/api/staff/shifts/swap-requests/route.ts:35-80
const swapRequestSchema = z.object({
requester_shift_id: z.string().uuid('Invalid shift ID'),
target_staff_id: z.string().uuid('Invalid staff ID'),
target_shift_id: z.string().uuid('Invalid target shift ID'),
reason: z.string().min(10, 'Please provide a reason (min 10 characters)').max(500)
})

Create Request Example

// Submit swap request
const requestSwap = async (request: SwapRequestData) => {
const response = await fetch('/api/staff/shifts/swap-requests', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
requester_shift_id: 'your-shift-uuid',
target_staff_id: 'colleague-uuid',
target_shift_id: 'their-shift-uuid',
reason: 'Medical appointment on Tuesday - would like to swap to Wednesday'
})
})

return response.json()
}

Response Format

// Swap request response
{
success: true,
data: {
id: "swap-request-uuid",
requester_shift_id: "your-shift-uuid",
target_staff_id: "colleague-uuid",
target_shift_id: "their-shift-uuid",
status: "pending_colleague",
reason: "Medical appointment on Tuesday",
created_at: "2025-01-14T10:00:00Z",
requester_shift: { /* shift details */ },
target_shift: { /* target shift details */ }
}
}

Swap Request Status

Status Values

StatusMeaningNext Step
pending_colleagueAwaiting colleague responseColleague accepts/declines
colleague_acceptedColleague agreedAwaiting manager approval
colleague_declinedColleague refusedRequest closed
pending_managerAwaiting managerManager approves/rejects
approvedManager approvedShifts exchanged
rejectedManager rejectedRequest closed
cancelledRequester cancelledRequest closed

Status Flow

pending_colleague → colleague_accepted → pending_manager → approved
↘ colleague_declined ↘ rejected
↘ cancelled

Viewing Swap Requests

List Your Requests

Endpoint: GET /api/staff/shifts/swap-requests

// Source: src/app/api/staff/shifts/swap-requests/route.ts:125-180
const listRequestsSchema = z.object({
status: z.enum([
'pending_colleague',
'colleague_accepted',
'colleague_declined',
'pending_manager',
'approved',
'rejected',
'cancelled'
]).optional(),
type: z.enum(['sent', 'received', 'all']).optional().default('all')
})

Query Parameters

ParameterOptionsDescription
statusSee aboveFilter by status
typesent, received, allFilter by direction

Example Requests

// Get all your swap requests
const getAllRequests = () => fetch('/api/staff/shifts/swap-requests')

// Get requests you sent
const getSentRequests = () => fetch('/api/staff/shifts/swap-requests?type=sent')

// Get requests sent to you
const getReceivedRequests = () => fetch('/api/staff/shifts/swap-requests?type=received')

// Get pending requests
const getPendingRequests = () => fetch('/api/staff/shifts/swap-requests?status=pending_colleague')

Response Structure

// Swap requests list response
{
success: true,
data: {
requests: [
{
id: "request-uuid",
status: "pending_colleague",
reason: "Medical appointment",
created_at: "2025-01-14T10:00:00Z",
requester: {
id: "staff-uuid",
first_name: "John",
last_name: "Smith"
},
requester_shift: {
id: "shift-uuid",
start_time: "2025-01-16T09:00:00Z",
end_time: "2025-01-16T17:30:00Z",
room: { name: "Room 1" }
},
target_shift: {
id: "target-shift-uuid",
start_time: "2025-01-17T09:00:00Z",
end_time: "2025-01-17T17:30:00Z",
room: { name: "Room 2" }
}
}
],
counts: {
pending_colleague: 2,
colleague_accepted: 1,
pending_manager: 0,
approved: 5,
rejected: 1
}
}
}

Responding to Swap Requests

Accept/Decline Request

Endpoint: PUT /api/staff/shifts/swap-requests/[requestId]/respond

// Source: src/app/api/staff/shifts/swap-requests/[requestId]/respond/route.ts:25-70
const respondSchema = z.object({
action: z.enum(['accept', 'decline']),
response_notes: z.string().max(500).optional()
})

Accept Request

// Accept a swap request
const acceptSwap = async (requestId: string) => {
const response = await fetch(`/api/staff/shifts/swap-requests/${requestId}/respond`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'accept',
response_notes: 'Happy to swap - works better for me too'
})
})

return response.json()
}

Decline Request

// Decline a swap request
const declineSwap = async (requestId: string, reason: string) => {
const response = await fetch(`/api/staff/shifts/swap-requests/${requestId}/respond`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'decline',
response_notes: reason
})
})

return response.json()
}

Manager Approval

Approval Process

After colleague accepts:

  1. Request status changes to pending_manager
  2. Manager receives notification
  3. Manager reviews both shifts
  4. Manager approves or rejects

What Managers Consider

FactorConsideration
CoverageWill coverage be maintained?
SkillsAre staff qualified for swapped shifts?
HoursDoes it affect overtime/limits?
FairnessIs the swap equitable?

Approval Outcome

// Approved - shifts are exchanged
{
status: "approved",
approved_at: "2025-01-15T14:00:00Z",
approved_by: "manager-uuid",
notes: "Approved - coverage maintained"
}

// Rejected
{
status: "rejected",
rejected_at: "2025-01-15T14:00:00Z",
rejected_by: "manager-uuid",
rejection_reason: "Would leave morning shift understaffed"
}

Finding Swap Partners

View Available Shifts

To find shifts to swap with:

// Fetch all company shifts for swap opportunities
const findSwapOpportunities = async (startDate: string, endDate: string) => {
const response = await fetch(
`/api/staff/shifts?start_date=${startDate}&end_date=${endDate}&include_all=true`
)

const { data } = await response.json()

// Filter to show others' shifts
return data.shifts.filter(shift => !shift.is_owner)
}

Suitable Swap Criteria

CriteriaDescription
Similar DurationComparable shift lengths
Compatible TimesTimes that work for both
Same WeekIdeally same pay period
Qualified StaffBoth can work each shift

Cancelling Requests

Cancel Your Request

You can cancel a pending request:

// Cancel swap request
const cancelSwapRequest = async (requestId: string) => {
const response = await fetch(`/api/staff/shifts/swap-requests/${requestId}/cancel`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
reason: 'No longer need to swap - appointment rescheduled'
})
})

return response.json()
}

Cancel Restrictions

StatusCan Cancel?
pending_colleague✅ Yes
colleague_accepted✅ Yes
pending_manager✅ Yes (with notice)
approved❌ No - contact manager
rejectedN/A - already closed
colleague_declinedN/A - already closed

Notifications

Swap Notifications

The system sends notifications at each stage:

EventNotification To
Request CreatedTarget colleague
Colleague AcceptsRequester, Manager
Colleague DeclinesRequester
Manager ApprovesBoth staff
Manager RejectsBoth staff
Request CancelledTarget colleague

Notification Example

// Notification for new swap request
{
type: "swap_request_received",
title: "Shift Swap Request",
message: "John Smith has requested to swap shifts with you",
data: {
requestId: "request-uuid",
requesterName: "John Smith",
yourShift: "Wed 15/01 09:00-17:30",
theirShift: "Tue 14/01 09:00-17:30"
},
created_at: "2025-01-14T10:00:00Z"
}

Swap Request Card

Display Component

// Swap request card component
const SwapRequestCard = ({ request, type }) => {
const isOutgoing = type === 'sent'

return (
<div className="card-glass p-4 space-y-4">
{/* Header */}
<div className="flex justify-between items-start">
<div>
<span className="text-sm text-secondary-text">
{isOutgoing ? 'Request to' : 'Request from'}
</span>
<div className="font-semibold">
{isOutgoing ? request.target.name : request.requester.name}
</div>
</div>
<SwapStatusBadge status={request.status} />
</div>

{/* Shifts comparison */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm text-secondary-text">Your Shift</label>
<ShiftSummary shift={isOutgoing ? request.requester_shift : request.target_shift} />
</div>
<div>
<label className="text-sm text-secondary-text">Their Shift</label>
<ShiftSummary shift={isOutgoing ? request.target_shift : request.requester_shift} />
</div>
</div>

{/* Reason */}
<div>
<label className="text-sm text-secondary-text">Reason</label>
<p className="text-sm">{request.reason}</p>
</div>

{/* Actions */}
{renderActions(request, type)}
</div>
)
}

Validation Rules

Request Validation

// Source: src/app/api/staff/shifts/swap-requests/route.ts:85-120
// Validation performed on swap request
const validateSwapRequest = async (data: SwapRequestData, staffId: string) => {
const errors = []

// 1. Verify ownership of requester shift
const { data: requesterShift } = await supabase
.from('shifts')
.select('*')
.eq('id', data.requester_shift_id)
.eq('staff_id', staffId)
.single()

if (!requesterShift) {
errors.push('You can only swap your own shifts')
}

// 2. Verify shift is in the future
if (requesterShift && moment(requesterShift.start_time).isBefore(moment())) {
errors.push('Cannot swap shifts in the past')
}

// 3. Verify target shift exists and belongs to target staff
const { data: targetShift } = await supabase
.from('shifts')
.select('*')
.eq('id', data.target_shift_id)
.eq('staff_id', data.target_staff_id)
.single()

if (!targetShift) {
errors.push('Target shift not found or does not belong to target staff')
}

// 4. Verify same company
if (requesterShift?.company_id !== targetShift?.company_id) {
errors.push('Can only swap with colleagues in the same company')
}

// 5. Verify shifts are confirmed
if (requesterShift?.status !== 'confirmed' || targetShift?.status !== 'confirmed') {
errors.push('Both shifts must be confirmed')
}

return { valid: errors.length === 0, errors }
}

Best Practices

For Successful Swaps

  1. Give Advance Notice - Request swaps early
  2. Provide Good Reason - Explain why you need to swap
  3. Choose Compatible Shifts - Similar duration and type
  4. Be Responsive - Reply to requests promptly
  5. Be Considerate - Don't make excessive requests

Swap Etiquette

DoDon't
Give clear reasonsMake vague requests
Respond promptlyLeave requests hanging
Be gratefulTake swaps for granted
Consider impactIgnore coverage needs

Troubleshooting

Common Issues

IssueCauseSolution
"Cannot swap past shift"Shift already occurredSelect future shift
"Shift not found"Invalid shift IDRefresh and retry
"Same company required"Different companiesCan only swap internally
"Already pending"Duplicate requestCancel existing first

Error Messages

ErrorMeaningAction
"Invalid shift ID"Shift doesn't existCheck shift selection
"You can only swap your own shifts"Not your shiftSelect your shift
"Both shifts must be confirmed"Draft or cancelledWait for confirmation
"Request already exists"DuplicateView existing request

Mobile View

Mobile Swap Interface

On mobile devices:

// Mobile swap request card
const MobileSwapCard = ({ request }) => (
<div className="p-4 border-b border-white/10">
<div className="flex justify-between items-center mb-3">
<span className="font-medium">{request.requester.name}</span>
<SwapStatusBadge status={request.status} size="sm" />
</div>

<div className="text-sm space-y-1">
<div>Their: {formatShiftBrief(request.requester_shift)}</div>
<div>Yours: {formatShiftBrief(request.target_shift)}</div>
</div>

{request.status === 'pending_colleague' && (
<div className="flex gap-2 mt-3">
<button className="flex-1 form-button secondary" onClick={() => decline(request.id)}>
Decline
</button>
<button className="flex-1 form-button" onClick={() => accept(request.id)}>
Accept
</button>
</div>
)}
</div>
)


Source Files:

  • src/app/api/staff/shifts/swap-requests/route.ts - Swap requests API
  • src/app/api/staff/shifts/swap-requests/[requestId]/respond/route.ts - Response API
  • src/components/staff/SwapRequestCard.tsx - Request card component
  • src/types/swap-request.types.ts - Type definitions