Skip to main content

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:

  1. All Requests - Complete history of your leave requests
  2. Status Tracking - See current status of each request
  3. Filtering - Filter by status, type, or date range
  4. Actions - Edit, cancel, or view details

Accessing Leave Requests

  1. Navigate to Staff PortalLeave from the sidebar
  2. The requests list shows your most recent requests
  3. 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

ParameterTypeRequiredDescription
statusstringNoFilter by request status
leave_typestringNoFilter by leave type
start_datestringNoFilter from this date (YYYY-MM-DD)
end_datestringNoFilter up to this date (YYYY-MM-DD)
limitnumberNoNumber of results (default: 50)
offsetnumberNoPagination 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

StatusBadgeDescription
pending🟡 YellowAwaiting manager review
approved🟢 GreenRequest approved
rejected🔴 RedRequest denied
cancelled⚫ GreyYou 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

StatusViewEditCancel
pending
approved⚠️ Contact manager
rejectedN/A
cancelledN/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

  1. Check Status Regularly - Monitor pending requests
  2. Review Rejections - Understand why requests were rejected
  3. Keep History - Use filters to review past requests
  4. Plan Ahead - Submit requests early for better approval chances

Request Management Tips

DoDon't
Check status regularlySubmit and forget
Review rejection reasonsResubmit without changes
Use filters effectivelyBrowse all requests manually
Cancel unused requestsLeave old requests pending

Troubleshooting

Common Issues

IssueCauseSolution
Request not appearingStill loadingWait or refresh
Wrong status shownCached dataRefresh page
Cannot cancelAlready approvedContact manager
Edit button missingNot pending statusCan only edit pending

Error Messages

ErrorMeaningAction
"Request not found"Invalid ID or deletedCheck URL
"Cannot modify"Status changedRefresh and check status
"Unauthorised"Not your requestCheck you're viewing your requests
"Session expired"Login timeoutLog in again


Source Files:

  • src/app/api/staff/leave-requests/route.ts - Leave requests API
  • src/app/api/staff/leave-requests/[requestId]/route.ts - Single request API
  • src/components/staff/LeaveRequestsList.tsx - Requests list component
  • src/components/staff/LeaveRequestCard.tsx - Request card component
  • src/types/leave.types.ts - Type definitions