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 Dashboard → Leave 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