Viewing Leave Requests
This guide covers how to view, track, and manage your leave requests, including filtering options, status tracking, and request history.
Overview
What is the Leave Requests View?
The leave requests view displays all your submitted time off requests:
- All Requests - Complete history of your leave requests
- Status Tracking - See current status of each request
- Filtering - Filter by status, type, or date range
- Actions - Edit, cancel, or view details
Accessing Leave Requests
- Navigate to Staff Portal → Leave from the sidebar
- The requests list shows your most recent requests
- Use filters to find specific requests
Leave Requests API
Fetch Your Requests
Endpoint: GET /api/staff/leave-requests
// Source: src/app/api/staff/leave-requests/route.ts:150-200
const leaveRequestsQuerySchema = z.object({
status: z.enum(['pending', 'approved', 'rejected', 'cancelled']).optional(),
leave_type: z.enum([
'annual', 'sick', 'emergency', 'compassionate',
'study', 'unpaid', 'maternity', 'paternity', 'toil'
]).optional(),
start_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
end_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
limit: z.coerce.number().min(1).max(100).optional().default(50),
offset: z.coerce.number().min(0).optional().default(0)
})
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| status | string | No | Filter by request status |
| leave_type | string | No | Filter by leave type |
| start_date | string | No | Filter from this date (YYYY-MM-DD) |
| end_date | string | No | Filter up to this date (YYYY-MM-DD) |
| limit | number | No | Number of results (default: 50) |
| offset | number | No | Pagination offset (default: 0) |
Example Request
// Fetch pending leave requests
const getPendingRequests = async () => {
const response = await fetch('/api/staff/leave-requests?status=pending')
return response.json()
}
// Fetch requests in date range
const getRequestsInRange = async (startDate: string, endDate: string) => {
const response = await fetch(
`/api/staff/leave-requests?start_date=${startDate}&end_date=${endDate}`
)
return response.json()
}
Response Format
// Leave requests response
{
success: true,
data: {
requests: [
{
id: "request-uuid-1",
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",
status: "pending",
half_day_start: false,
half_day_end: false,
created_at: "2025-01-14T10:00:00Z",
updated_at: "2025-01-14T10:00:00Z",
reviewed_at: null,
reviewed_by: null,
rejection_reason: null
},
{
id: "request-uuid-2",
leave_type: "sick",
start_date: "2025-01-10",
end_date: "2025-01-10",
total_days: 1.0,
reason: "Unwell",
status: "approved",
reviewed_at: "2025-01-10T09:00:00Z",
reviewed_by: "manager-uuid"
}
],
meta: {
total: 25,
limit: 50,
offset: 0,
has_more: false
},
summary: {
pending: 1,
approved: 15,
rejected: 3,
cancelled: 6
}
}
}
Request Status
Status Values
| Status | Badge | Description |
|---|---|---|
| pending | 🟡 Yellow | Awaiting manager review |
| approved | 🟢 Green | Request approved |
| rejected | 🔴 Red | Request denied |
| cancelled | ⚫ Grey | You cancelled the request |
Status Badge Component
// Status badge display
const LeaveStatusBadge = ({ status }: { status: LeaveStatus }) => {
const config = {
pending: {
colour: 'bg-yellow-500/20 text-yellow-400',
icon: Clock,
label: 'Pending'
},
approved: {
colour: 'bg-green-500/20 text-green-400',
icon: Check,
label: 'Approved'
},
rejected: {
colour: 'bg-red-500/20 text-red-400',
icon: X,
label: 'Rejected'
},
cancelled: {
colour: 'bg-gray-500/20 text-gray-400',
icon: Ban,
label: 'Cancelled'
}
}
const { colour, icon: Icon, label } = config[status]
return (
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full ${colour}`}>
<Icon className="w-3.5 h-3.5" />
<span className="text-sm font-medium">{label}</span>
</span>
)
}
Filtering Requests
Filter by Status
// Filter requests by status
const FilteredRequestsList = () => {
const [statusFilter, setStatusFilter] = useState<LeaveStatus | 'all'>('all')
const { data, isLoading } = useLeaveRequests({
status: statusFilter === 'all' ? undefined : statusFilter
})
return (
<div>
{/* Status filter tabs */}
<div className="flex gap-2 mb-4">
{['all', 'pending', 'approved', 'rejected', 'cancelled'].map(status => (
<button
key={status}
onClick={() => setStatusFilter(status as LeaveStatus | 'all')}
className={`px-4 py-2 rounded-lg transition-colors ${
statusFilter === status
? 'bg-coral-gradient text-white'
: 'bg-white/5 text-secondary-text hover:bg-white/10'
}`}
>
{status.charAt(0).toUpperCase() + status.slice(1)}
</button>
))}
</div>
{/* Requests list */}
<RequestsList requests={data?.requests} isLoading={isLoading} />
</div>
)
}
Filter by Leave Type
// Filter by leave type
const getAnnualLeaveRequests = () => {
return fetch('/api/staff/leave-requests?leave_type=annual')
}
const getSickLeaveRequests = () => {
return fetch('/api/staff/leave-requests?leave_type=sick')
}
Filter by Date Range
// Filter by date range
const getRequestsThisYear = async () => {
const startOfYear = moment().startOf('year').format('YYYY-MM-DD')
const endOfYear = moment().endOf('year').format('YYYY-MM-DD')
const response = await fetch(
`/api/staff/leave-requests?start_date=${startOfYear}&end_date=${endOfYear}`
)
return response.json()
}
Request Card Display
Request Card Component
// Leave request card
const LeaveRequestCard = ({ request }: { request: LeaveRequest }) => {
const startDate = moment(request.start_date)
const endDate = moment(request.end_date)
const isSingleDay = startDate.isSame(endDate, 'day')
return (
<div className="card-glass p-4 space-y-4">
{/* Header */}
<div className="flex justify-between items-start">
<div>
<h3 className="font-semibold text-lg">
{formatLeaveType(request.leave_type)}
</h3>
<p className="text-sm text-secondary-text">
{isSingleDay
? startDate.format('dddd, D MMMM YYYY')
: `${startDate.format('D MMM')} - ${endDate.format('D MMM YYYY')}`
}
</p>
</div>
<LeaveStatusBadge status={request.status} />
</div>
{/* Details */}
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-secondary-text">Duration</span>
<div className="font-medium">
{request.total_days} {request.total_days === 1 ? 'day' : 'days'}
</div>
</div>
<div>
<span className="text-secondary-text">Submitted</span>
<div className="font-medium">
{moment(request.created_at).format('D MMM YYYY')}
</div>
</div>
</div>
{/* Reason */}
<div>
<span className="text-sm text-secondary-text">Reason</span>
<p className="text-sm mt-1">{request.reason}</p>
</div>
{/* Rejection reason if applicable */}
{request.status === 'rejected' && request.rejection_reason && (
<div className="p-3 bg-red-500/10 rounded-lg">
<span className="text-sm text-red-400 font-medium">Rejection Reason</span>
<p className="text-sm mt-1">{request.rejection_reason}</p>
</div>
)}
{/* Actions */}
{request.status === 'pending' && (
<div className="flex gap-2 pt-2 border-t border-white/10">
<button
onClick={() => handleEdit(request.id)}
className="form-button secondary flex-1"
>
Edit
</button>
<button
onClick={() => handleCancel(request.id)}
className="form-button secondary flex-1 text-red-400"
>
Cancel
</button>
</div>
)}
</div>
)
}
Requests List View
List Layout
// Requests list component
const LeaveRequestsList = ({ requests, isLoading }) => {
if (isLoading) {
return <RequestsListSkeleton />
}
if (!requests || requests.length === 0) {
return <EmptyRequestsState />
}
return (
<div className="space-y-4">
{requests.map(request => (
<LeaveRequestCard key={request.id} request={request} />
))}
</div>
)
}
Empty State
// Empty state when no requests
const EmptyRequestsState = () => (
<div className="card-glass p-8 text-center">
<Calendar className="w-12 h-12 mx-auto text-secondary-text" />
<h3 className="mt-4 font-semibold">No Leave Requests</h3>
<p className="text-secondary-text mt-2">
You haven't submitted any leave requests yet.
</p>
<button
onClick={() => router.push('/staff/leave/request')}
className="form-button mt-4"
>
Request Leave
</button>
</div>
)
Request Details
View Single Request
Endpoint: GET /api/staff/leave-requests/[requestId]
// Fetch single request details
const getRequestDetails = async (requestId: string) => {
const response = await fetch(`/api/staff/leave-requests/${requestId}`)
return response.json()
}
Detailed Response
// Single request detail 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: "approved",
half_day_start: false,
half_day_end: false,
created_at: "2025-01-14T10:00:00Z",
updated_at: "2025-01-15T14:00:00Z",
reviewed_at: "2025-01-15T14:00:00Z",
reviewed_by: "manager-uuid",
reviewer: {
id: "manager-uuid",
first_name: "Sarah",
last_name: "Jones"
},
timeline: [
{
action: "created",
timestamp: "2025-01-14T10:00:00Z",
actor: "John Smith"
},
{
action: "approved",
timestamp: "2025-01-15T14:00:00Z",
actor: "Sarah Jones",
notes: "Approved - enjoy your holiday"
}
]
}
}
Request Detail Panel
// Request detail panel
const LeaveRequestDetail = ({ requestId }: { requestId: string }) => {
const { data: request, isLoading } = useLeaveRequest(requestId)
if (isLoading) return <DetailPanelSkeleton />
if (!request) return <NotFoundState />
return (
<div className="card-glass p-6 space-y-6">
{/* Header */}
<div className="flex justify-between items-start">
<div>
<h2 className="text-xl font-semibold">
{formatLeaveType(request.leave_type)}
</h2>
<p className="text-secondary-text">
Request #{request.id.slice(0, 8)}
</p>
</div>
<LeaveStatusBadge status={request.status} />
</div>
{/* Dates */}
<div className="p-4 bg-white/5 rounded-lg">
<div className="grid grid-cols-2 gap-4">
<div>
<span className="text-sm text-secondary-text">Start Date</span>
<div className="font-medium">
{moment(request.start_date).format('dddd, D MMMM YYYY')}
{request.half_day_start && (
<span className="text-sm text-secondary-text ml-2">(PM only)</span>
)}
</div>
</div>
<div>
<span className="text-sm text-secondary-text">End Date</span>
<div className="font-medium">
{moment(request.end_date).format('dddd, D MMMM YYYY')}
{request.half_day_end && (
<span className="text-sm text-secondary-text ml-2">(AM only)</span>
)}
</div>
</div>
</div>
<div className="mt-4 pt-4 border-t border-white/10">
<span className="text-sm text-secondary-text">Total Duration</span>
<div className="text-2xl font-bold">
{request.total_days} {request.total_days === 1 ? 'day' : 'days'}
</div>
</div>
</div>
{/* Reason */}
<div>
<h3 className="text-sm font-medium text-secondary-text mb-2">Reason</h3>
<p>{request.reason}</p>
</div>
{/* Timeline */}
<div>
<h3 className="text-sm font-medium text-secondary-text mb-2">Timeline</h3>
<RequestTimeline events={request.timeline} />
</div>
{/* Actions */}
{request.status === 'pending' && (
<div className="flex gap-3 pt-4 border-t border-white/10">
<button
onClick={() => handleEdit(request.id)}
className="form-button secondary flex-1"
>
Edit Request
</button>
<button
onClick={() => handleCancel(request.id)}
className="form-button secondary flex-1 text-red-400"
>
Cancel Request
</button>
</div>
)}
</div>
)
}
Request Timeline
Timeline Display
// Request timeline component
const RequestTimeline = ({ events }: { events: TimelineEvent[] }) => (
<div className="space-y-4">
{events.map((event, index) => (
<div key={index} className="flex gap-4">
<div className="flex flex-col items-center">
<div className={`w-3 h-3 rounded-full ${getEventColor(event.action)}`} />
{index < events.length - 1 && (
<div className="w-0.5 h-full bg-white/10 mt-1" />
)}
</div>
<div className="pb-4">
<div className="font-medium">{formatAction(event.action)}</div>
<div className="text-sm text-secondary-text">
{moment(event.timestamp).format('D MMM YYYY, HH:mm')} by {event.actor}
</div>
{event.notes && (
<p className="text-sm mt-1 text-secondary-text italic">
"{event.notes}"
</p>
)}
</div>
</div>
))}
</div>
)
const getEventColor = (action: string) => {
switch (action) {
case 'created': return 'bg-blue-500'
case 'approved': return 'bg-green-500'
case 'rejected': return 'bg-red-500'
case 'cancelled': return 'bg-gray-500'
case 'edited': return 'bg-yellow-500'
default: return 'bg-gray-500'
}
}
Summary Statistics
Request Summary
// Summary statistics display
const LeaveRequestsSummary = ({ summary }) => (
<div className="grid grid-cols-4 gap-4">
<div className="card-glass p-4 text-center">
<div className="text-2xl font-bold text-yellow-400">{summary.pending}</div>
<div className="text-sm text-secondary-text">Pending</div>
</div>
<div className="card-glass p-4 text-center">
<div className="text-2xl font-bold text-green-400">{summary.approved}</div>
<div className="text-sm text-secondary-text">Approved</div>
</div>
<div className="card-glass p-4 text-center">
<div className="text-2xl font-bold text-red-400">{summary.rejected}</div>
<div className="text-sm text-secondary-text">Rejected</div>
</div>
<div className="card-glass p-4 text-center">
<div className="text-2xl font-bold text-gray-400">{summary.cancelled}</div>
<div className="text-sm text-secondary-text">Cancelled</div>
</div>
</div>
)
Pagination
Paginated Results
// Pagination component
const PaginatedRequestsList = () => {
const [page, setPage] = useState(0)
const limit = 10
const { data, isLoading } = useLeaveRequests({
limit,
offset: page * limit
})
return (
<div>
<LeaveRequestsList requests={data?.requests} isLoading={isLoading} />
{/* Pagination controls */}
<div className="flex justify-between items-center mt-4 pt-4 border-t border-white/10">
<button
onClick={() => setPage(p => Math.max(0, p - 1))}
disabled={page === 0}
className="form-button secondary"
>
Previous
</button>
<span className="text-sm text-secondary-text">
Showing {page * limit + 1} - {Math.min((page + 1) * limit, data?.meta.total || 0)} of {data?.meta.total || 0}
</span>
<button
onClick={() => setPage(p => p + 1)}
disabled={!data?.meta.has_more}
className="form-button secondary"
>
Next
</button>
</div>
</div>
)
}
Search Requests
Search Functionality
// Search requests
const SearchableRequestsList = () => {
const [searchQuery, setSearchQuery] = useState('')
const [dateRange, setDateRange] = useState<DateRange | null>(null)
const { data, isLoading } = useLeaveRequests({
start_date: dateRange?.start,
end_date: dateRange?.end
})
// Client-side search filtering
const filteredRequests = useMemo(() => {
if (!data?.requests || !searchQuery) return data?.requests
const query = searchQuery.toLowerCase()
return data.requests.filter(request =>
request.reason.toLowerCase().includes(query) ||
request.leave_type.toLowerCase().includes(query)
)
}, [data?.requests, searchQuery])
return (
<div className="space-y-4">
{/* Search input */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-secondary-text" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search requests..."
className="form-input pl-10 w-full"
/>
</div>
{/* Date range filter */}
<DateRangePicker
value={dateRange}
onChange={setDateRange}
/>
{/* Results */}
<LeaveRequestsList
requests={filteredRequests}
isLoading={isLoading}
/>
</div>
)
}
Mobile View
Mobile Requests List
// Mobile requests view
const MobileRequestsList = () => (
<div className="p-4 space-y-4">
{/* Filter tabs - horizontal scroll */}
<div className="flex gap-2 overflow-x-auto pb-2">
{['All', 'Pending', 'Approved', 'Rejected'].map(status => (
<button
key={status}
className="px-4 py-2 rounded-full whitespace-nowrap text-sm bg-white/5"
>
{status}
</button>
))}
</div>
{/* Request cards */}
<div className="space-y-3">
{requests.map(request => (
<MobileRequestCard key={request.id} request={request} />
))}
</div>
</div>
)
// Mobile request card - compact
const MobileRequestCard = ({ request }) => (
<div
className="p-4 bg-white/5 rounded-lg active:bg-white/10"
onClick={() => viewRequest(request.id)}
>
<div className="flex justify-between items-start">
<div>
<div className="font-medium">{formatLeaveType(request.leave_type)}</div>
<div className="text-sm text-secondary-text">
{moment(request.start_date).format('D MMM')} -
{moment(request.end_date).format('D MMM')}
</div>
</div>
<LeaveStatusBadge status={request.status} size="sm" />
</div>
<div className="mt-2 text-sm text-secondary-text">
{request.total_days} {request.total_days === 1 ? 'day' : 'days'}
</div>
</div>
)
Actions
Available Actions
| Status | View | Edit | Cancel |
|---|---|---|---|
| pending | ✅ | ✅ | ✅ |
| approved | ✅ | ❌ | ⚠️ Contact manager |
| rejected | ✅ | ❌ | N/A |
| cancelled | ✅ | ❌ | N/A |
Edit Request
// Edit pending request
const editRequest = 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()
}
Cancel Request
// Cancel request
const cancelRequest = async (requestId: string, reason: string) => {
const response = await fetch(`/api/staff/leave-requests/${requestId}/cancel`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reason })
})
return response.json()
}
Best Practices
For Managing Requests
- Check Status Regularly - Monitor pending requests
- Review Rejections - Understand why requests were rejected
- Keep History - Use filters to review past requests
- Plan Ahead - Submit requests early for better approval chances
Request Management Tips
| Do | Don't |
|---|---|
| Check status regularly | Submit and forget |
| Review rejection reasons | Resubmit without changes |
| Use filters effectively | Browse all requests manually |
| Cancel unused requests | Leave old requests pending |
Troubleshooting
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Request not appearing | Still loading | Wait or refresh |
| Wrong status shown | Cached data | Refresh page |
| Cannot cancel | Already approved | Contact manager |
| Edit button missing | Not pending status | Can only edit pending |
Error Messages
| Error | Meaning | Action |
|---|---|---|
| "Request not found" | Invalid ID or deleted | Check URL |
| "Cannot modify" | Status changed | Refresh and check status |
| "Unauthorised" | Not your request | Check you're viewing your requests |
| "Session expired" | Login timeout | Log in again |
Related Documentation
- Requesting Leave - Submit new requests
- Leave Balance - Check entitlement
- Dashboard - Staff dashboard
- Notifications - Alert settings
Source Files:
src/app/api/staff/leave-requests/route.ts- Leave requests APIsrc/app/api/staff/leave-requests/[requestId]/route.ts- Single request APIsrc/components/staff/LeaveRequestsList.tsx- Requests list componentsrc/components/staff/LeaveRequestCard.tsx- Request card componentsrc/types/leave.types.ts- Type definitions