Skip to main content

Approving Leave

This guide covers the complete leave request approval workflow, including reviewing requests, approving or rejecting leave, adding manager notes, and managing conflict detection.


Overview

Leave Approval Responsibilities

Company Managers are responsible for:

  1. Reviewing Requests - Evaluate leave requests from staff
  2. Checking Conflicts - Verify scheduling compatibility
  3. Approving/Rejecting - Make decisions on requests
  4. Adding Notes - Document reasons for decisions
  5. Monitoring Balances - Ensure staff have sufficient entitlement

Access Requirements

RoleCan ApproveCan RejectCan View All
System Admin✅ Yes✅ Yes✅ Yes
Company Manager✅ Yes✅ Yes✅ Yes
Staff❌ No❌ NoOwn only

Accessing Leave Management

  1. Navigate to DashboardLeave Management
  2. Or access directly via /dashboard/leave

Leave Management Dashboard

// Source: src/components/leave/LeaveManagement.tsx:10-21
export function LeaveManagement() {
const { user } = useAuth()
const [requests, setRequests] = useState<LeaveRequestComponentType[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [filterStatus, setFilterStatus] = useState<string>('all')
const [companyId, setCompanyId] = useState<string | null>(null)

// Modal state
const [selectedRequest, setSelectedRequest] = useState<LeaveRequestComponentType | null>(null)
const [showApprovalModal, setShowApprovalModal] = useState(false)
const [isProcessing, setIsProcessing] = useState(false)
}

Dashboard Statistics

Request Summary Cards

The dashboard displays four summary cards:

// Source: src/components/leave/LeaveManagement.tsx:157-192
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="card-glass-hover rounded-xl">
<div className="flex items-center gap-3 mb-2">
<Clock className="w-5 h-5 text-yellow-400" />
<h3 className="text-primary-text font-semibold">Pending</h3>
</div>
<div className="text-2xl font-bold text-yellow-400">
{requests.filter(r => r.status === 'pending').length}
</div>
</div>
// ... Approved, Rejected, Total cards
</div>
CardColourShows
PendingYellow (#F59E0B)Requests awaiting decision
ApprovedGreen (#10B981)Approved requests
RejectedRed (#EF4444)Rejected requests
TotalCoral (#FFB5B0)All requests

Filtering Requests

Status Filter

// Source: src/components/leave/LeaveManagement.tsx:194-208
<div className="card-glass rounded-xl p-6">
<div className="flex gap-3">
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
className="form-input"
>
<option value="all">All Requests</option>
<option value="pending">Pending</option>
<option value="approved">Approved</option>
<option value="rejected">Rejected</option>
</select>
</div>
</div>

Filter Options

FilterShows
All RequestsEvery leave request
PendingRequests awaiting approval
ApprovedPreviously approved requests
RejectedPreviously rejected requests

Request List Display

Request Card Information

Each leave request displays:

// Source: src/components/leave/LeaveManagement.tsx:212-267
<div key={request.id} className="card-glass-hover rounded-xl cursor-pointer">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div
className="w-4 h-4 rounded-full flex-shrink-0"
style={{ backgroundColor: request.leaveTypeColor }}
/>
<div className="flex-1">
<h3 className="text-primary-text font-semibold">
{request.staffName}
</h3>
<p className="text-secondary-text text-sm">
{request.leaveTypeName}{request.daysDisplay}
</p>
<p className="text-secondary-text text-sm">
{request.dateRange}
</p>
</div>
</div>
</div>
</div>

Displayed Fields

FieldDescription
Staff NameEmployee requesting leave
Leave TypeType with colour indicator
DaysDuration (e.g., "5 days")
Date RangeStart and end dates (DD/MM/YYYY)
ReasonStaff's reason for request
StatusCurrent request status
ApproverWho approved/rejected (if applicable)

Approval Modal

Opening the Modal

Click on any request to open the approval modal:

// Source: src/components/leave/LeaveManagement.tsx:60-63
const handleRequestClick = (request: LeaveRequestComponentType) => {
setSelectedRequest(request)
setShowApprovalModal(true)
}

The approval modal contains:

  1. Staff Information - Employee details
  2. Leave Details - Type, dates, duration
  3. Request Information - Reason, notes, timestamps
  4. Manager Response - Previous admin notes (if any)
  5. Action Buttons - Approve/Reject options
// Source: src/components/leave/LeaveApprovalModal.tsx:121-164
<div className="p-6 space-y-6">
{/* Staff Information */}
<div className="card-glass-hover rounded-lg p-4">
<div className="flex items-center gap-3 mb-3">
<User className="w-5 h-5 text-accent-coral" />
<h3 className="text-white font-semibold">Staff Member</h3>
</div>
<p className="text-lg font-medium text-white">{request.staffName}</p>
</div>

{/* Leave Details */}
<div className="card-glass-hover rounded-lg p-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<span className="text-secondary-text text-sm">Leave Type:</span>
<div className="flex items-center gap-2 mt-1">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: request.leaveTypeColor }}
/>
<div className="text-white font-medium">{request.leaveTypeName}</div>
</div>
</div>
// ... Duration, Dates, Status
</div>
</div>
</div>

Approving a Request

Approval Process

  1. Click on a Pending request to open the modal
  2. Review staff information and leave details
  3. Click Approve Request button
  4. Optionally add manager notes
  5. Click Approve Request to confirm
// Source: src/components/leave/LeaveApprovalModal.tsx:36-43
const handleApprovalAction = (action: 'approve' | 'reject') => {
setShowApprovalForm(action)
setFormError(null)
// Pre-fill with existing admin notes if any
if (request?.adminNotes) {
setAdminNotes(request.adminNotes)
}
}

API Call

// Source: src/components/leave/LeaveManagement.tsx:65-103
const handleApproval = async (requestId: string, approved: boolean, adminNotes: string) => {
if (!companyId) {
throw new Error('No company ID available')
}

setIsProcessing(true)

try {
const response = await fetch(`/api/companies/${companyId}/leave-requests/${requestId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
status: approved ? 'approved' : 'rejected',
admin_notes: adminNotes
})
})

if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.message || 'Failed to update leave request')
}

await fetchLeaveRequests()
setShowApprovalModal(false)
setSelectedRequest(null)
} catch (error) {
console.error('Failed to update leave request:', error)
throw error
} finally {
setIsProcessing(false)
}
}

Backend Processing

The API uses atomic RPC functions:

// Source: src/app/api/companies/[companyId]/leave-requests/[requestId]/route.ts:106-122
// Call appropriate RPC
const rpcName = newStatus === 'approved' ? 'approve_leave_request' : 'reject_leave_request'
const rpcParams = newStatus === 'approved'
? { p_request_id: params.requestId, p_approved_by: managerStaff.id, p_admin_notes: validationResult.data.admin_notes ?? undefined }
: { p_request_id: params.requestId, p_rejected_by: managerStaff.id, p_admin_notes: validationResult.data.admin_notes ?? undefined }

const { error: rpcError } = await supabase.rpc(rpcName, rpcParams)

Rejecting a Request

Rejection Process

  1. Click on a Pending request to open the modal
  2. Review the request details
  3. Click Reject Request button
  4. Add rejection reason (required)
  5. Click Reject Request to confirm

Required Notes for Rejection

// Source: src/components/leave/LeaveApprovalModal.tsx:45-58
const handleSubmitApproval = async () => {
if (!request || !showApprovalForm) return

// Validate admin notes for rejections (required)
if (showApprovalForm === 'reject' && !adminNotes.trim()) {
setFormError('Admin notes are required when rejecting a leave request')
return
}

// Validate admin notes length
if (adminNotes.length > 1000) {
setFormError('Admin notes must be 1000 characters or less')
return
}
// ... proceed with rejection
}
Rejection Requires Reason

When rejecting a leave request, you must provide a reason in the manager notes field. This helps staff understand why their request was declined.


Manager Notes

Adding Notes

The manager notes field supports up to 1000 characters:

// Source: src/components/leave/LeaveApprovalModal.tsx:242-256
<div className="mb-4">
<label className="block text-sm font-medium text-white mb-2">
Manager Notes {showApprovalForm === 'reject' && <span className="text-red-400">*</span>}
</label>
<textarea
value={adminNotes}
onChange={(e) => setAdminNotes(e.target.value)}
className="form-input w-full h-24 resize-none"
placeholder={
showApprovalForm === 'approve'
? "Optional notes about this approval..."
: "Please provide a reason for rejection..."
}
disabled={isProcessing}
/>
<div className="text-xs text-secondary-text mt-1">
{adminNotes.length}/1000 characters
</div>
</div>

Notes Requirements

ActionNotes RequiredMax Length
Approve❌ Optional1000 chars
Reject✅ Required1000 chars

Notification System

Staff Notifications

When a request is approved or rejected, staff receive notifications:

// Source: src/app/api/companies/[companyId]/leave-requests/[requestId]/route.ts:128-145
// Notify staff via in-app notification
try {
const notificationType = newStatus === 'approved' ? 'LEAVE_REQUEST_APPROVED' : 'LEAVE_REQUEST_REJECTED'
const title = newStatus === 'approved' ? 'Leave Request Approved' : 'Leave Request Rejected'
const message = newStatus === 'approved'
? `Your ${result.data.leaveTypeName} request for ${result.data.startDate} - ${result.data.endDate} has been approved.`
: `Your ${result.data.leaveTypeName} request for ${result.data.startDate} - ${result.data.endDate} has been rejected.${result.data.adminNotes ? ` Reason: ${result.data.adminNotes}` : ''}`

await createNotification({
recipientId: result.data.staffId,
companyId: result.data.companyId,
type: notificationType,
title,
message,
relatedEntityId: params.requestId
})
} catch (notificationError) {
console.warn('[LeaveRequestDecision] Failed to send staff notification:', notificationError)
}

Email Notifications

If email is enabled, staff also receive email notifications:

// Source: src/app/api/companies/[companyId]/leave-requests/[requestId]/route.ts:147-183
if (isEmailEnabled()) {
const emailResult = await sendEmail({
to: staffInfo.email,
subject: `Leave Request ${newStatus === 'approved' ? 'Approved' : 'Rejected'} - ${companyInfo?.name || 'Shyfts'}`,
react: LeaveRequestDecisionEmail({
recipientName: staffInfo.fullName || result.data.staffName,
companyName: companyInfo?.name || 'Shyfts',
leaveType: result.data.leaveTypeName,
startDate: result.data.startDate,
endDate: result.data.endDate,
daysRequested: result.data.daysRequested,
status: newStatus,
decidedBy,
adminNotes: result.data.adminNotes || undefined,
dashboardUrl
}),
template: 'leave-request-decision'
})
}

Conflict Detection

Automatic Conflict Checking

The system checks for scheduling conflicts before processing:

// Source: src/lib/db/leave-request.db.ts:310-332
export async function checkLeaveConflicts(params: {
staff_id: string
start_date: string
end_date: string
exclude_id?: string
}): Promise<DBLeaveRequest[]> {
const { result } = await trackPerformance('checkLeaveConflicts', async () => {
const supabase = await createClient()

// Use RPC for UK timezone-safe date comparison
const { data, error } = await supabase.rpc('check_leave_conflicts', {
p_staff_id: params.staff_id,
p_start_date: params.start_date,
p_end_date: params.end_date,
p_exclude_id: params.exclude_id ?? undefined
})

if (error) handleDBError(error)
return data || []
})

return result
}

Conflict Error Handling

// Source: src/app/api/companies/[companyId]/leave-requests/[requestId]/route.ts:114-121
if (rpcError) {
if (rpcError.message.includes('conflicts')) {
return ApiResponses.conflict(rpcError.message)
}
if (rpcError.message.includes('status is') || rpcError.message.includes('not pending')) {
return ApiResponses.badRequest(rpcError.message)
}
throw rpcError
}

Request Status Flow

Status Transitions

┌─────────┐     Approve      ┌──────────┐
│ Pending │ ────────────────▶│ Approved │
└─────────┘ └──────────┘

│ Reject ┌──────────┐
└──────────────────────▶│ Rejected │
└──────────┘

Status Colours

// Source: src/lib/adapters/leave-request.adapter.ts:291-299
private getStatusColor(status: string): string {
const colorMap: Record<string, string> = {
'pending': '#F59E0B',
'approved': '#10B981',
'rejected': '#EF4444',
'cancelled': '#6B7280',
'withdrawn': '#6B7280'
}
return colorMap[status.toLowerCase()] || '#6B7280'
}
StatusColourHex
PendingYellow#F59E0B
ApprovedGreen#10B981
RejectedRed#EF4444
CancelledGrey#6B7280
WithdrawnGrey#6B7280

API Reference

Get Leave Request

Endpoint: GET /api/companies/[companyId]/leave-requests/[requestId]

Response:

{
"success": true,
"data": {
"id": "request-uuid",
"staffName": "John Smith",
"leaveTypeName": "Annual Leave",
"leaveTypeColor": "#10B981",
"startDate": "01/01/2025",
"endDate": "05/01/2025",
"dateRange": "01/01/2025 - 05/01/2025",
"daysRequested": 5,
"daysDisplay": "5 days",
"status": "pending",
"statusDisplay": "Pending",
"statusColor": "#F59E0B",
"reason": "Family holiday",
"adminNotes": null,
"approvedByName": null,
"approvedAt": null
}
}

Update Leave Request (Approve/Reject)

Endpoint: PATCH /api/companies/[companyId]/leave-requests/[requestId]

Request Body:

{
"status": "approved",
"admin_notes": "Approved - enjoy your holiday!"
}

Response:

{
"success": true,
"data": {
"id": "request-uuid",
"status": "approved",
"statusDisplay": "Approved",
"approvedByName": "Jane Manager",
"approvedAt": "13/01/2025"
}
}

Best Practices

Before Approving

  1. Check Staff Entitlement - Ensure sufficient leave balance
  2. Review Schedule Impact - Consider shift coverage requirements
  3. Check for Conflicts - Verify no scheduling clashes
  4. Consider Team Coverage - Ensure adequate staffing levels
  5. Review Dates - Confirm dates are reasonable

When Rejecting

  1. Provide Clear Reason - Document why the request was rejected
  2. Be Professional - Keep notes constructive and helpful
  3. Suggest Alternatives - Offer alternative dates if possible
  4. Be Timely - Don't leave requests pending unnecessarily
  5. Follow Up - Discuss with staff if needed

General Guidelines

PracticeDescription
Timely ResponseProcess requests within 48 hours
Consistent ApplicationApply same standards to all staff
Clear CommunicationAlways add notes explaining decisions
Document ProperlyUse notes for audit trail
Consider WorkloadBalance team coverage needs

Troubleshooting

Common Issues

IssueSolution
Cannot approve requestCheck you have Company Manager permissions
Request not appearingVerify filter settings, refresh page
Conflict errorCheck for overlapping approved leave
Notes not savingEnsure notes are under 1000 characters
Status not updatingCheck network connection, try again

Error Messages

ErrorCauseSolution
"Request is no longer pending"Already processedRequest was handled by another manager
"Conflict detected"Overlapping leaveReview existing approved leave
"Insufficient permissions"Wrong roleContact System Admin for access
"Notes required"Missing rejection reasonAdd reason before rejecting


Source Files:

  • src/components/leave/LeaveManagement.tsx - Leave management dashboard
  • src/components/leave/LeaveApprovalModal.tsx - Approval modal component
  • src/lib/db/leave-request.db.ts - Leave request database operations
  • src/lib/adapters/leave-request.adapter.ts - Leave request data adapter
  • src/app/api/companies/[companyId]/leave-requests/[requestId]/route.ts - Leave request API