Skip to main content

Requesting Leave

This guide covers how to submit leave requests, select leave types, specify dates, and understand the approval process.


Overview

What is a Leave Request?

A leave request is a formal submission for time off from work:

  1. Select Leave Type - Choose the type of leave needed
  2. Specify Dates - Enter start and end dates
  3. Add Reason - Provide context for the request
  4. Submit - Send for manager approval
  5. Await Decision - Manager reviews and approves/rejects

Request Workflow

Create Request → Submit → Manager Review → Approved/Rejected
↘ (If rejected) → Edit → Resubmit

Leave Types

Available Leave Types

TypeDescriptionDeducts Balance
Annual LeaveStandard holiday entitlement✅ Yes
Sick LeaveIllness or medical appointments❌ No
Emergency LeaveUrgent personal circumstances❌ No
Compassionate LeaveBereavement or family crisis❌ No
Study LeaveTraining or educationVaries
Unpaid LeaveLeave without pay❌ No
Maternity/PaternityParental leave❌ No
TOILTime off in lieuUses TOIL balance

Leave Type Selection

// Leave types configuration
const leaveTypes = [
{ id: 'annual', name: 'Annual Leave', deductsBalance: true },
{ id: 'sick', name: 'Sick Leave', deductsBalance: false },
{ id: 'emergency', name: 'Emergency Leave', deductsBalance: false },
{ id: 'compassionate', name: 'Compassionate Leave', deductsBalance: false },
{ id: 'study', name: 'Study Leave', deductsBalance: true },
{ id: 'unpaid', name: 'Unpaid Leave', deductsBalance: false },
{ id: 'maternity', name: 'Maternity Leave', deductsBalance: false },
{ id: 'paternity', name: 'Paternity Leave', deductsBalance: false },
{ id: 'toil', name: 'TOIL (Time Off In Lieu)', deductsBalance: false }
]

Creating a Leave Request

Prerequisites

Before requesting leave:

RequirementDescription
Active EmploymentMust be an active staff member
Available BalanceSufficient leave balance (for annual leave)
Advance NoticeRequests should be made in advance
No ConflictsCheck for scheduling conflicts

Leave Request API

Endpoint: POST /api/staff/leave-requests

// Source: src/app/api/staff/leave-requests/route.ts:35-80
const leaveRequestSchema = z.object({
leave_type: z.enum([
'annual',
'sick',
'emergency',
'compassionate',
'study',
'unpaid',
'maternity',
'paternity',
'toil'
]),
start_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Use YYYY-MM-DD format'),
end_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Use YYYY-MM-DD format'),
reason: z.string().min(10, 'Please provide a reason (min 10 characters)').max(500),
half_day_start: z.boolean().optional().default(false),
half_day_end: z.boolean().optional().default(false)
})

Submit Request Example

// Submit a leave request
const submitLeaveRequest = async (request: LeaveRequestData) => {
const response = await fetch('/api/staff/leave-requests', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
leave_type: 'annual',
start_date: '2025-02-10',
end_date: '2025-02-14',
reason: 'Family holiday - visiting relatives in Scotland',
half_day_start: false,
half_day_end: false
})
})

return response.json()
}

Response Format

// Leave request response
{
success: true,
data: {
id: "request-uuid",
staff_id: "staff-uuid",
company_id: "company-uuid",
leave_type: "annual",
start_date: "2025-02-10",
end_date: "2025-02-14",
total_days: 5.0,
reason: "Family holiday - visiting relatives in Scotland",
status: "pending",
half_day_start: false,
half_day_end: false,
created_at: "2025-01-14T10:00:00Z"
}
}

Date Selection

Selecting Date Range

When selecting dates:

FieldFormatDescription
Start DateDD/MM/YYYYFirst day of leave
End DateDD/MM/YYYYLast day of leave (inclusive)

Half Days

Request partial days off:

// Half day options
const HalfDayOptions = () => (
<div className="space-y-2">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="half_day_start"
checked={halfDayStart}
onChange={(e) => setHalfDayStart(e.target.checked)}
/>
<label htmlFor="half_day_start">Half day on start date (PM only)</label>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="half_day_end"
checked={halfDayEnd}
onChange={(e) => setHalfDayEnd(e.target.checked)}
/>
<label htmlFor="half_day_end">Half day on end date (AM only)</label>
</div>
</div>
)

Days Calculation

// Calculate total leave days
const calculateLeaveDays = (
startDate: string,
endDate: string,
halfDayStart: boolean,
halfDayEnd: boolean
): number => {
const start = moment(startDate)
const end = moment(endDate)

let totalDays = 0
const current = start.clone()

while (current.isSameOrBefore(end)) {
// Skip weekends
if (current.isoWeekday() <= 5) {
totalDays += 1
}
current.add(1, 'day')
}

// Adjust for half days
if (halfDayStart) totalDays -= 0.5
if (halfDayEnd) totalDays -= 0.5

return totalDays
}

Weekend Handling

The system automatically excludes weekends:

DayCounted?
Monday - Friday✅ Yes
Saturday❌ No
Sunday❌ No

Leave Request Form

Form Component

// Leave request form component
const LeaveRequestForm = () => {
const [leaveType, setLeaveType] = useState<LeaveType>('annual')
const [startDate, setStartDate] = useState('')
const [endDate, setEndDate] = useState('')
const [reason, setReason] = useState('')
const [halfDayStart, setHalfDayStart] = useState(false)
const [halfDayEnd, setHalfDayEnd] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)

const totalDays = useMemo(() => {
if (!startDate || !endDate) return 0
return calculateLeaveDays(startDate, endDate, halfDayStart, halfDayEnd)
}, [startDate, endDate, halfDayStart, halfDayEnd])

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)

try {
const response = await fetch('/api/staff/leave-requests', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
leave_type: leaveType,
start_date: startDate,
end_date: endDate,
reason,
half_day_start: halfDayStart,
half_day_end: halfDayEnd
})
})

const data = await response.json()

if (data.success) {
toast.success('Leave request submitted successfully')
router.push('/staff/leave')
} else {
toast.error(data.error || 'Failed to submit request')
}
} catch (error) {
toast.error('Failed to submit request')
} finally {
setIsSubmitting(false)
}
}

return (
<form onSubmit={handleSubmit} className="card-glass p-6 space-y-6">
{/* Leave Type */}
<div>
<label className="block text-sm font-medium mb-2">Leave Type</label>
<select
value={leaveType}
onChange={(e) => setLeaveType(e.target.value as LeaveType)}
className="form-input w-full"
required
>
{leaveTypes.map(type => (
<option key={type.id} value={type.id}>{type.name}</option>
))}
</select>
</div>

{/* Date Range */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-2">Start Date</label>
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="form-input w-full"
min={moment().format('YYYY-MM-DD')}
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">End Date</label>
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="form-input w-full"
min={startDate || moment().format('YYYY-MM-DD')}
required
/>
</div>
</div>

{/* Half Day Options */}
<HalfDayOptions
halfDayStart={halfDayStart}
halfDayEnd={halfDayEnd}
onHalfDayStartChange={setHalfDayStart}
onHalfDayEndChange={setHalfDayEnd}
/>

{/* Total Days Display */}
<div className="p-4 bg-white/5 rounded-lg">
<div className="text-sm text-secondary-text">Total Days</div>
<div className="text-2xl font-bold">{totalDays} days</div>
</div>

{/* Reason */}
<div>
<label className="block text-sm font-medium mb-2">Reason</label>
<textarea
value={reason}
onChange={(e) => setReason(e.target.value)}
className="form-input w-full h-24"
placeholder="Please provide a reason for your leave request..."
minLength={10}
maxLength={500}
required
/>
<div className="text-xs text-secondary-text mt-1">
{reason.length}/500 characters
</div>
</div>

{/* Submit Button */}
<button
type="submit"
disabled={isSubmitting || totalDays === 0}
className="form-button w-full"
>
{isSubmitting ? 'Submitting...' : 'Submit Leave Request'}
</button>
</form>
)
}

Validation Rules

Pre-Submission Validation

// Source: src/app/api/staff/leave-requests/route.ts:85-140
// Validation performed on leave request
const validateLeaveRequest = async (
data: LeaveRequestData,
staffId: string
): Promise<ValidationResult> => {
const errors: string[] = []
const warnings: string[] = []

// 1. Date validation
const startDate = moment(data.start_date)
const endDate = moment(data.end_date)

if (startDate.isBefore(moment(), 'day')) {
errors.push('Start date cannot be in the past')
}

if (endDate.isBefore(startDate)) {
errors.push('End date must be on or after start date')
}

// 2. Balance check for annual leave
if (data.leave_type === 'annual') {
const balance = await getLeaveBalance(staffId)
const requestedDays = calculateLeaveDays(
data.start_date,
data.end_date,
data.half_day_start,
data.half_day_end
)

if (requestedDays > balance.remaining) {
errors.push(`Insufficient balance. Available: ${balance.remaining} days`)
}
}

// 3. Overlap check
const { data: existingRequests } = await supabase
.from('leave_requests')
.select('*')
.eq('staff_id', staffId)
.in('status', ['pending', 'approved'])
.or(`start_date.lte.${data.end_date},end_date.gte.${data.start_date}`)

if (existingRequests && existingRequests.length > 0) {
errors.push('You already have a leave request for these dates')
}

// 4. Advance notice warning
const daysNotice = startDate.diff(moment(), 'days')
if (daysNotice < 14 && data.leave_type === 'annual') {
warnings.push('Requests should be made at least 2 weeks in advance')
}

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

Validation Error Display

ErrorCauseSolution
"Start date cannot be in the past"Past date selectedSelect today or future date
"Insufficient balance"Not enough annual leaveRequest fewer days or different type
"Leave request already exists"Overlapping datesChoose different dates
"End date before start date"Date order wrongCorrect the date range

Balance Check

Check Before Submitting

// Check balance before submitting
const BalanceCheck = ({ leaveType, requestedDays }) => {
const { data: balance, isLoading } = useLeaveBalance()

if (isLoading) return <Spinner />

const hasInsufficientBalance =
leaveType === 'annual' && requestedDays > balance.remaining

return (
<div className={`p-4 rounded-lg ${
hasInsufficientBalance ? 'bg-red-500/10' : 'bg-white/5'
}`}>
<div className="flex justify-between">
<span>Current Balance</span>
<span className="font-semibold">{balance.remaining} days</span>
</div>
<div className="flex justify-between mt-2">
<span>Requested</span>
<span className="font-semibold">-{requestedDays} days</span>
</div>
<div className="border-t border-white/10 mt-2 pt-2 flex justify-between">
<span>After Request</span>
<span className={`font-semibold ${
hasInsufficientBalance ? 'text-red-400' : ''
}`}>
{balance.remaining - requestedDays} days
</span>
</div>
{hasInsufficientBalance && (
<p className="text-sm text-red-400 mt-2">
You do not have sufficient balance for this request.
</p>
)}
</div>
)
}

Approval Process

What Happens After Submission

  1. Request Created - Status set to pending
  2. Manager Notified - Alert sent to manager
  3. Review Period - Manager reviews request
  4. Decision Made - Approved or rejected
  5. Notification Sent - You receive the outcome

Request Status Values

StatusMeaningNext Step
pendingAwaiting manager reviewWait for decision
approvedRequest approvedTake your leave
rejectedRequest deniedSee rejection reason
cancelledYou cancelled requestCan submit new request

Manager Considerations

When reviewing requests, managers consider:

FactorDescription
CoverageWill there be enough staff?
NoticeWas request made in advance?
BalanceDoes employee have entitlement?
Team ImpactAre others off at same time?
Business NeedsAny critical events during period?

Editing Requests

Edit Pending Requests

You can edit requests that are still pending:

// Update pending leave request
const updateLeaveRequest = async (requestId: string, updates: Partial<LeaveRequestData>) => {
const response = await fetch(`/api/staff/leave-requests/${requestId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
})

return response.json()
}

Edit Restrictions

StatusCan Edit?
pending✅ Yes
approved❌ No - contact manager
rejected❌ No - submit new request
cancelledN/A

Cancelling Requests

Cancel a Request

// Cancel leave request
const cancelLeaveRequest = async (requestId: string) => {
const response = await fetch(`/api/staff/leave-requests/${requestId}/cancel`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
reason: 'Plans changed - no longer need this time off'
})
})

return response.json()
}

Cancellation Rules

StatusCan Cancel?Notes
pending✅ YesImmediate cancellation
approved⚠️ MaybeRequires manager approval
rejectedN/AAlready closed
past dates❌ NoCannot cancel historical leave

Notifications

Request Notifications

EventNotification To
Request SubmittedManager
Request ApprovedYou (staff)
Request RejectedYou (staff)
Request CancelledManager (if was approved)

Notification Example

// Leave request notification
{
type: "leave_request_submitted",
title: "Leave Request",
message: "John Smith has requested 5 days annual leave",
data: {
requestId: "request-uuid",
staffName: "John Smith",
leaveType: "annual",
startDate: "10/02/2025",
endDate: "14/02/2025",
totalDays: 5
},
created_at: "2025-01-14T10:00:00Z"
}

Emergency Leave

Requesting Emergency Leave

For urgent situations:

// Emergency leave request
const requestEmergencyLeave = async () => {
const response = await fetch('/api/staff/leave-requests', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
leave_type: 'emergency',
start_date: moment().format('YYYY-MM-DD'),
end_date: moment().format('YYYY-MM-DD'),
reason: 'Family emergency - need to attend immediately',
is_urgent: true
})
})

return response.json()
}

Emergency Leave Guidelines

GuidelineDescription
Notify ASAPContact manager as soon as possible
Submit RequestCreate formal request for records
Provide UpdatesKeep manager informed
DocumentationMay be required depending on circumstances

Sick Leave

Recording Sick Leave

For illness or medical appointments:

// Sick leave request
const requestSickLeave = async (dates: { start: string; end: string }) => {
const response = await fetch('/api/staff/leave-requests', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
leave_type: 'sick',
start_date: dates.start,
end_date: dates.end,
reason: 'Unwell - unable to work',
self_certification: dates.end === dates.start ||
moment(dates.end).diff(moment(dates.start), 'days') <= 7
})
})

return response.json()
}

Sick Leave Documentation

DurationDocumentation
1-7 daysSelf-certification
8+ daysFit note from GP required

Mobile View

Mobile Leave Request

// Mobile leave request form
const MobileLeaveRequestForm = () => (
<div className="p-4 space-y-6">
<h2 className="text-lg font-semibold">Request Leave</h2>

{/* Leave type select */}
<select className="form-input w-full text-base">
<option>Annual Leave</option>
<option>Sick Leave</option>
<option>Emergency Leave</option>
{/* ... other options */}
</select>

{/* Date inputs with native mobile pickers */}
<div className="space-y-4">
<input
type="date"
className="form-input w-full text-base"
placeholder="Start Date"
/>
<input
type="date"
className="form-input w-full text-base"
placeholder="End Date"
/>
</div>

{/* Reason textarea */}
<textarea
className="form-input w-full text-base h-24"
placeholder="Reason for leave..."
/>

{/* Submit button - 44px+ touch target */}
<button className="form-button w-full h-12">
Submit Request
</button>
</div>
)

Best Practices

For Successful Requests

  1. Plan Ahead - Request annual leave well in advance
  2. Check Calendar - Avoid peak periods if possible
  3. Clear Reason - Provide helpful context
  4. Check Balance - Ensure you have sufficient entitlement
  5. Notify Team - Inform colleagues of your absence

Request Tips

DoDon't
Give advance noticeRequest last minute
Check team calendarIgnore coverage needs
Provide clear reasonLeave reason vague
Monitor request statusForget to check response

Troubleshooting

Common Issues

IssueCauseSolution
"Insufficient balance"Not enough days leftCheck balance first
"Dates overlap"Existing request for datesCancel or modify existing
"Cannot submit"Missing required fieldsComplete all fields
"Past date selected"Start date in pastSelect today or later

Error Messages

ErrorMeaningAction
"Invalid date format"Wrong date formatUse correct format
"Reason too short"Less than 10 charactersAdd more detail
"Leave type invalid"Unknown leave typeSelect from options
"Request not found"Invalid request IDRefresh and retry


Source Files:

  • src/app/api/staff/leave-requests/route.ts - Leave requests API
  • src/components/staff/LeaveRequestForm.tsx - Request form component
  • src/types/leave.types.ts - Type definitions
  • src/lib/utils/leave.ts - Leave calculation utilities