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:
- Request Swap - Ask a colleague to swap shifts
- Accept/Decline - Colleague responds to request
- Manager Approval - Manager reviews and approves
- 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:
| Requirement | Description |
|---|---|
| Future Shift | Cannot swap past shifts |
| Confirmed Status | Shift must be confirmed |
| Same Company | Colleague must be in same company |
| Available Colleague | Target 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
| Status | Meaning | Next Step |
|---|---|---|
| pending_colleague | Awaiting colleague response | Colleague accepts/declines |
| colleague_accepted | Colleague agreed | Awaiting manager approval |
| colleague_declined | Colleague refused | Request closed |
| pending_manager | Awaiting manager | Manager approves/rejects |
| approved | Manager approved | Shifts exchanged |
| rejected | Manager rejected | Request closed |
| cancelled | Requester cancelled | Request 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
| Parameter | Options | Description |
|---|---|---|
| status | See above | Filter by status |
| type | sent, received, all | Filter 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:
- Request status changes to
pending_manager - Manager receives notification
- Manager reviews both shifts
- Manager approves or rejects
What Managers Consider
| Factor | Consideration |
|---|---|
| Coverage | Will coverage be maintained? |
| Skills | Are staff qualified for swapped shifts? |
| Hours | Does it affect overtime/limits? |
| Fairness | Is 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
| Criteria | Description |
|---|---|
| Similar Duration | Comparable shift lengths |
| Compatible Times | Times that work for both |
| Same Week | Ideally same pay period |
| Qualified Staff | Both 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
| Status | Can Cancel? |
|---|---|
| pending_colleague | ✅ Yes |
| colleague_accepted | ✅ Yes |
| pending_manager | ✅ Yes (with notice) |
| approved | ❌ No - contact manager |
| rejected | N/A - already closed |
| colleague_declined | N/A - already closed |
Notifications
Swap Notifications
The system sends notifications at each stage:
| Event | Notification To |
|---|---|
| Request Created | Target colleague |
| Colleague Accepts | Requester, Manager |
| Colleague Declines | Requester |
| Manager Approves | Both staff |
| Manager Rejects | Both staff |
| Request Cancelled | Target 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
- Give Advance Notice - Request swaps early
- Provide Good Reason - Explain why you need to swap
- Choose Compatible Shifts - Similar duration and type
- Be Responsive - Reply to requests promptly
- Be Considerate - Don't make excessive requests
Swap Etiquette
| Do | Don't |
|---|---|
| Give clear reasons | Make vague requests |
| Respond promptly | Leave requests hanging |
| Be grateful | Take swaps for granted |
| Consider impact | Ignore coverage needs |
Troubleshooting
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| "Cannot swap past shift" | Shift already occurred | Select future shift |
| "Shift not found" | Invalid shift ID | Refresh and retry |
| "Same company required" | Different companies | Can only swap internally |
| "Already pending" | Duplicate request | Cancel existing first |
Error Messages
| Error | Meaning | Action |
|---|---|---|
| "Invalid shift ID" | Shift doesn't exist | Check shift selection |
| "You can only swap your own shifts" | Not your shift | Select your shift |
| "Both shifts must be confirmed" | Draft or cancelled | Wait for confirmation |
| "Request already exists" | Duplicate | View 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>
)
Related Documentation
- Viewing Shifts - See your schedule
- Shift Details - Shift information
- Dashboard - Staff dashboard
- Notifications - Alert settings
Source Files:
src/app/api/staff/shifts/swap-requests/route.ts- Swap requests APIsrc/app/api/staff/shifts/swap-requests/[requestId]/respond/route.ts- Response APIsrc/components/staff/SwapRequestCard.tsx- Request card componentsrc/types/swap-request.types.ts- Type definitions