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 Portal → Leave 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
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​

  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