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:
- Select Leave Type - Choose the type of leave needed
- Specify Dates - Enter start and end dates
- Add Reason - Provide context for the request
- Submit - Send for manager approval
- 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
| Type | Description | Deducts Balance |
|---|---|---|
| Annual Leave | Standard holiday entitlement | ✅ Yes |
| Sick Leave | Illness or medical appointments | ❌ No |
| Emergency Leave | Urgent personal circumstances | ❌ No |
| Compassionate Leave | Bereavement or family crisis | ❌ No |
| Study Leave | Training or education | Varies |
| Unpaid Leave | Leave without pay | ❌ No |
| Maternity/Paternity | Parental leave | ❌ No |
| TOIL | Time off in lieu | Uses 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:
| Requirement | Description |
|---|---|
| Active Employment | Must be an active staff member |
| Available Balance | Sufficient leave balance (for annual leave) |
| Advance Notice | Requests should be made in advance |
| No Conflicts | Check 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:
| Field | Format | Description |
|---|---|---|
| Start Date | DD/MM/YYYY | First day of leave |
| End Date | DD/MM/YYYY | Last 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:
| Day | Counted? |
|---|---|
| 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
| Error | Cause | Solution |
|---|---|---|
| "Start date cannot be in the past" | Past date selected | Select today or future date |
| "Insufficient balance" | Not enough annual leave | Request fewer days or different type |
| "Leave request already exists" | Overlapping dates | Choose different dates |
| "End date before start date" | Date order wrong | Correct 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
- Request Created - Status set to
pending - Manager Notified - Alert sent to manager
- Review Period - Manager reviews request
- Decision Made - Approved or rejected
- Notification Sent - You receive the outcome
Request Status Values
| Status | Meaning | Next Step |
|---|---|---|
| pending | Awaiting manager review | Wait for decision |
| approved | Request approved | Take your leave |
| rejected | Request denied | See rejection reason |
| cancelled | You cancelled request | Can submit new request |
Manager Considerations
When reviewing requests, managers consider:
| Factor | Description |
|---|---|
| Coverage | Will there be enough staff? |
| Notice | Was request made in advance? |
| Balance | Does employee have entitlement? |
| Team Impact | Are others off at same time? |
| Business Needs | Any 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
| Status | Can Edit? |
|---|---|
| pending | ✅ Yes |
| approved | ❌ No - contact manager |
| rejected | ❌ No - submit new request |
| cancelled | N/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
| Status | Can Cancel? | Notes |
|---|---|---|
| pending | ✅ Yes | Immediate cancellation |
| approved | ⚠️ Maybe | Requires manager approval |
| rejected | N/A | Already closed |
| past dates | ❌ No | Cannot cancel historical leave |
Notifications
Request Notifications
| Event | Notification To |
|---|---|
| Request Submitted | Manager |
| Request Approved | You (staff) |
| Request Rejected | You (staff) |
| Request Cancelled | Manager (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
| Guideline | Description |
|---|---|
| Notify ASAP | Contact manager as soon as possible |
| Submit Request | Create formal request for records |
| Provide Updates | Keep manager informed |
| Documentation | May 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
| Duration | Documentation |
|---|---|
| 1-7 days | Self-certification |
| 8+ days | Fit 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
- Plan Ahead - Request annual leave well in advance
- Check Calendar - Avoid peak periods if possible
- Clear Reason - Provide helpful context
- Check Balance - Ensure you have sufficient entitlement
- Notify Team - Inform colleagues of your absence
Request Tips
| Do | Don't |
|---|---|
| Give advance notice | Request last minute |
| Check team calendar | Ignore coverage needs |
| Provide clear reason | Leave reason vague |
| Monitor request status | Forget to check response |
Troubleshooting
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| "Insufficient balance" | Not enough days left | Check balance first |
| "Dates overlap" | Existing request for dates | Cancel or modify existing |
| "Cannot submit" | Missing required fields | Complete all fields |
| "Past date selected" | Start date in past | Select today or later |
Error Messages
| Error | Meaning | Action |
|---|---|---|
| "Invalid date format" | Wrong date format | Use correct format |
| "Reason too short" | Less than 10 characters | Add more detail |
| "Leave type invalid" | Unknown leave type | Select from options |
| "Request not found" | Invalid request ID | Refresh and retry |
Related Documentation
- Viewing Requests - Track your requests
- Leave Balance - Check entitlement
- Dashboard - Staff dashboard
- Viewing Shifts - See your schedule
Source Files:
src/app/api/staff/leave-requests/route.ts- Leave requests APIsrc/components/staff/LeaveRequestForm.tsx- Request form componentsrc/types/leave.types.ts- Type definitionssrc/lib/utils/leave.ts- Leave calculation utilities