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:
- Reviewing Requests - Evaluate leave requests from staff
- Checking Conflicts - Verify scheduling compatibility
- Approving/Rejecting - Make decisions on requests
- Adding Notes - Document reasons for decisions
- Monitoring Balances - Ensure staff have sufficient entitlement
Access Requirements
| Role | Can Approve | Can Reject | Can View All |
|---|---|---|---|
| System Admin | ✅ Yes | ✅ Yes | ✅ Yes |
| Company Manager | ✅ Yes | ✅ Yes | ✅ Yes |
| Staff | ❌ No | ❌ No | Own only |
Accessing Leave Management
Navigation
- Navigate to Dashboard → Leave Management
- 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>
| Card | Colour | Shows |
|---|---|---|
| Pending | Yellow (#F59E0B) | Requests awaiting decision |
| Approved | Green (#10B981) | Approved requests |
| Rejected | Red (#EF4444) | Rejected requests |
| Total | Coral (#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
| Filter | Shows |
|---|---|
| All Requests | Every leave request |
| Pending | Requests awaiting approval |
| Approved | Previously approved requests |
| Rejected | Previously 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
| Field | Description |
|---|---|
| Staff Name | Employee requesting leave |
| Leave Type | Type with colour indicator |
| Days | Duration (e.g., "5 days") |
| Date Range | Start and end dates (DD/MM/YYYY) |
| Reason | Staff's reason for request |
| Status | Current request status |
| Approver | Who 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)
}
Modal Sections
The approval modal contains:
- Staff Information - Employee details
- Leave Details - Type, dates, duration
- Request Information - Reason, notes, timestamps
- Manager Response - Previous admin notes (if any)
- 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
- Click on a Pending request to open the modal
- Review staff information and leave details
- Click Approve Request button
- Optionally add manager notes
- 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
- Click on a Pending request to open the modal
- Review the request details
- Click Reject Request button
- Add rejection reason (required)
- 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
}
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
| Action | Notes Required | Max Length |
|---|---|---|
| Approve | ❌ Optional | 1000 chars |
| Reject | ✅ Required | 1000 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'
}
| Status | Colour | Hex |
|---|---|---|
| Pending | Yellow | #F59E0B |
| Approved | Green | #10B981 |
| Rejected | Red | #EF4444 |
| Cancelled | Grey | #6B7280 |
| Withdrawn | Grey | #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
- Check Staff Entitlement - Ensure sufficient leave balance
- Review Schedule Impact - Consider shift coverage requirements
- Check for Conflicts - Verify no scheduling clashes
- Consider Team Coverage - Ensure adequate staffing levels
- Review Dates - Confirm dates are reasonable
When Rejecting
- Provide Clear Reason - Document why the request was rejected
- Be Professional - Keep notes constructive and helpful
- Suggest Alternatives - Offer alternative dates if possible
- Be Timely - Don't leave requests pending unnecessarily
- Follow Up - Discuss with staff if needed
General Guidelines
| Practice | Description |
|---|---|
| Timely Response | Process requests within 48 hours |
| Consistent Application | Apply same standards to all staff |
| Clear Communication | Always add notes explaining decisions |
| Document Properly | Use notes for audit trail |
| Consider Workload | Balance team coverage needs |
Troubleshooting
Common Issues
| Issue | Solution |
|---|---|
| Cannot approve request | Check you have Company Manager permissions |
| Request not appearing | Verify filter settings, refresh page |
| Conflict error | Check for overlapping approved leave |
| Notes not saving | Ensure notes are under 1000 characters |
| Status not updating | Check network connection, try again |
Error Messages
| Error | Cause | Solution |
|---|---|---|
| "Request is no longer pending" | Already processed | Request was handled by another manager |
| "Conflict detected" | Overlapping leave | Review existing approved leave |
| "Insufficient permissions" | Wrong role | Contact System Admin for access |
| "Notes required" | Missing rejection reason | Add reason before rejecting |
Related Documentation
- Leave Types - Configure leave types
- Leave Reports - View leave analytics
- Staff Management - Manage staff records
Source Files:
src/components/leave/LeaveManagement.tsx- Leave management dashboardsrc/components/leave/LeaveApprovalModal.tsx- Approval modal componentsrc/lib/db/leave-request.db.ts- Leave request database operationssrc/lib/adapters/leave-request.adapter.ts- Leave request data adaptersrc/app/api/companies/[companyId]/leave-requests/[requestId]/route.ts- Leave request API