Skip to main content

Leave Reports

This guide covers the leave analysis reporting features in Shyfts, including accessing reports, understanding metrics, filtering data, and exporting leave information.


Overview

Leave Analysis Report

The Leave Analysis report provides comprehensive insights into staff leave patterns:

  1. Leave Summary - Total days taken, approved, pending, rejected
  2. Leave Breakdown - Days by leave type
  3. Leave Patterns - Trends and seasonal patterns
  4. Staff Comparison - Leave usage across team members

Report Type Definition

// Source: src/types/reporting.ts:17-22
export type ReportType =
| 'STAFF_PERFORMANCE'
| 'SCHEDULING_ANALYTICS'
| 'ATTENDANCE_SUMMARY'
| 'HOURS_TRACKING'
| 'LEAVE_ANALYSIS' // Leave reports
| 'SKILLS_MATRIX'
| 'COST_ANALYSIS'
| 'COMPLIANCE_REPORT'

Accessing Leave Reports

From Reporting Dashboard

  1. Navigate to DashboardReports
  2. Select the Leave tab from the report types
// Source: src/components/reporting/ReportingDashboard.tsx:491-496
const tabs = [
{ id: 'overview', label: 'Overview', icon: BarChart3 },
{ id: 'performance', label: 'Performance', icon: TrendingUp },
{ id: 'attendance', label: 'Attendance', icon: Clock },
{ id: 'leave', label: 'Leave', icon: Calendar },
{ id: 'cost', label: 'Cost', icon: DollarSign },
{ id: 'compliance', label: 'Compliance', icon: ShieldCheck }
]

Report Header

// Source: src/components/reporting/ReportingDashboard.tsx:893-894
<h3 className="text-xl font-semibold text-white">Leave Analysis</h3>

Leave Analysis Data Structure

LeaveAnalysisData Interface

// Source: src/types/reporting.ts:306-335
export interface LeaveAnalysisData {
staff_id: string
staff_name: string
role: string
leave_summary: {
total_leave_days: number
approved_leave_days: number
pending_requests: number
rejected_requests: number
leave_balance: number
leave_utilization_rate: number
}
leave_breakdown: Array<{
leave_type: string
days_taken: number
days_remaining: number
utilization_percentage: number
}>
leave_patterns: {
most_common_leave_type: string
average_leave_duration: number
seasonal_trends: Array<{
month: string
leave_requests: number
}>
}
leave_taken_by_type: {
[key: string]: number
}
}

Report Metrics

Leave Summary Metrics

MetricDescriptionSource
Total Leave DaysAll leave days in periodSum of all leave requests
Approved DaysDays with approved statusApproved leave requests
Pending RequestsRequests awaiting approvalStatus = 'pending'
Rejected RequestsDeclined requestsStatus = 'rejected'
Leave BalanceDays remainingEntitlement - Used - Pending
Utilisation RatePercentage of allowance used(Used / Entitlement) x 100

Summary Calculation

// Source: src/services/reportingService.ts:1007-1014
leave_summary: {
total_leave_days: Object.values(leaveByType).reduce((sum, data) => sum + data.taken, 0),
approved_leave_days: Object.values(leaveByType).reduce((sum, data) => sum + data.approved, 0),
pending_requests: leaveRequests?.filter(r => r.status === 'pending').length || 0,
rejected_requests: leaveRequests?.filter(r => r.status === 'rejected').length || 0,
leave_balance: 0, // Calculated from entitlement data
leave_utilization_rate: 0
}

Report Display

Leave Analysis Table

The report displays in a data table format:

// Source: src/components/reporting/ReportingDashboard.tsx:907-926
<table className="w-full text-sm">
<thead>
<tr className="border-b border-white/20">
<th className="text-left py-3 px-4 text-white font-semibold">Staff Name</th>
<th className="text-right py-3 px-4 text-white font-semibold">Approved Days</th>
<th className="text-right py-3 px-4 text-white font-semibold">Pending</th>
<th className="text-right py-3 px-4 text-white font-semibold">Rejected</th>
</tr>
</thead>
<tbody>
{leaveData.map((row) => (
<tr key={row.staff_id} className="border-b border-white/10 hover:bg-white/5">
<td className="py-3 px-4 text-white">{row.staff_name}</td>
<td className="py-3 px-4 text-right text-white">{row.leave_summary.approved_leave_days}</td>
<td className="py-3 px-4 text-right text-white">{row.leave_summary.pending_requests}</td>
<td className="py-3 px-4 text-right text-white">{row.leave_summary.rejected_requests}</td>
</tr>
))}
</tbody>
</table>

Table Columns

ColumnDescription
Staff NameEmployee full name
Approved DaysNumber of approved leave days
PendingCount of pending requests
RejectedCount of rejected requests

Empty State

// Source: src/components/reporting/ReportingDashboard.tsx:929-936
{leaveData.length === 0 && (
<div className="text-center py-8">
<div className="text-secondary-text mb-2">No leave data available</div>
<div className="text-sm text-white/60">
Try expanding the date range or clearing filters.
</div>
</div>
)}

Date Range Selection

Period Types

Reports can be generated for various date ranges:

// Source: src/types/reporting.ts:27-31
export interface DateRange {
start_date: string
end_date: string
period_type: 'DAY' | 'WEEK' | 'MONTH' | 'QUARTER' | 'YEAR' | 'CUSTOM'
}

Common Date Ranges

PeriodDescription
DaySingle day report
Week7-day period
MonthCalendar month
Quarter3-month period
YearFull year (often calendar or financial year)
CustomUser-defined start and end dates

Filtering Reports

Available Filters

// Source: src/types/reporting.ts:59-67
export interface ReportFilters {
staff_ids?: string[] // Filter by specific staff
role_ids?: string[] // Filter by role
employment_types?: string[] // Filter by employment type
leave_type_ids?: string[] // Filter by leave type
}

Leave Type Filter

Filter reports by specific leave types:

// Source: src/types/reporting.ts:64
leave_type_ids?: string[] // leave_requests.leave_type_id → leave_types.id

Example Filter Usage

{
"staff_ids": ["staff-uuid-1", "staff-uuid-2"],
"leave_type_ids": ["annual-leave-uuid"]
}

Pagination

Pagination State

// Source: src/components/reporting/ReportingDashboard.tsx:77-79
const [leavePage, setLeavePage] = useState(1)
const [leavePageSize, setLeavePageSize] = useState(50)
const [leavePagination, setLeavePagination] = useState<PaginationMeta | null>(null)

Pagination Controls

// Source: src/components/reporting/ReportingDashboard.tsx:895-903
{renderPaginationControls({
label: 'Leave',
pagination: leavePagination,
page: leavePage,
pageSize: leavePageSize,
setPage: setLeavePage,
setPageSize: setLeavePageSize
})}

Page Size Options

OptionRecords Per Page
10Quick scan
25Default view
50Extended view
100Full team view

Data Fetching

Fetch Leave Data Function

// Source: src/components/reporting/ReportingDashboard.tsx:217-233
const fetchLeaveData = React.useCallback(async (options?: { cacheBypass?: boolean }) => {
try {
const result = await runReport<LeaveAnalysisData[]>('LEAVE_ANALYSIS', {
...options,
pagination: { page: leavePage, pageSize: leavePageSize }
})
setLeaveData(result.data)
setLeavePagination(result.pagination || null)
const charts = ReportingService.generateChartData(result.data, 'leave_analysis')
setChartData(prev => ({ ...prev, leave: charts }))
} catch (error) {
console.error('Failed to fetch leave analysis data:', error)
setLeaveData([])
setLeavePagination(null)
throw error
}
}, [runReport, leavePage, leavePageSize])

Report API Call

// Source: src/components/reporting/ReportingDashboard.tsx:99-105
const runReport = React.useCallback(
async <T,>(
reportType: ReportType,
options?: { cacheBypass?: boolean; pagination?: { page: number; pageSize: number } }
): Promise<{ data: T; pagination?: PaginationMeta }> => {
const response = await fetch('/api/reports/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
reportType,
companyId: user?.company_id,
dateRange,
filters,
pagination: options?.pagination,
cacheBypass: options?.cacheBypass
})
})
// ...
}
)

Leave Breakdown

Leave by Type

The report breaks down leave by type:

// Source: src/services/reportingService.ts:1001-1006
leave_breakdown: Object.entries(leaveByType).map(([type, data]) => ({
leave_type: type,
days_taken: data.taken,
days_remaining: 0, // Would need entitlement data
utilization_percentage: 0
}))

Breakdown Fields

FieldDescription
leave_typeName of the leave type
days_takenDays used for this type
days_remainingDays left for this type
utilization_percentage% of type allowance used

Export Options

Available Export Formats

// Source: src/components/reporting/ReportingDashboard.tsx:1094-1114
<button onClick={() => handleExport('CSV')}>
Export as CSV
</button>
<button onClick={() => handleExport('PDF')}>
Export as PDF
</button>
<button onClick={() => handleExport('EXCEL')}>
Export as Excel
</button>

Export Formats

FormatUse CaseFile Extension
CSVData analysis, spreadsheet import.csv
PDFPrinted reports, formal documentation.pdf
ExcelAdvanced analysis, charts.xlsx

Export Function

// Source: src/components/reporting/ReportingDashboard.tsx:403-485
const handleExport = async (format: 'PDF' | 'EXCEL' | 'CSV') => {
setExportStatus(null)

let reportData: unknown
let reportTitle = ''

switch (activeTab) {
case 'leave':
reportData = leaveData
reportTitle = 'Leave_Analysis_Report'
break
// ... other cases
}

try {
const result = await ReportingService.exportReport(reportData, format, reportTitle)

if (result.success) {
setExportStatus({
success: true,
message: `Successfully exported as ${format}. File: ${result.file_name}`
})
}
} catch (error) {
setExportStatus({
success: false,
message: error instanceof Error ? error.message : 'Export failed'
})
}
}

Export Status Display

// Source: src/components/reporting/ReportingDashboard.tsx:1117-1129
{exportStatus && (
<div className={`mt-4 p-3 rounded-lg ${
exportStatus.success ? 'bg-green-500/20' : 'bg-red-500/20'
}`}>
<p className={`text-sm ${
exportStatus.success ? 'text-green-400' : 'text-red-400'
}`}>
{exportStatus.message}
</p>
</div>
)}

Report Generation

Server-Side Report Generation

// Source: src/services/reportingService.ts:884-892
static async generateLeaveAnalysisReport(
db: DbClient,
companyId: string,
dateRange: DateRange,
filters: ReportFilters = {}
): Promise<LeaveAnalysisData[]> {
const result = await this.generateLeaveAnalysisReportPaginated(
db, companyId, dateRange, filters
)
return result.data
}

Data Query

// Source: src/services/reportingService.ts:943-955
const { data: allLeaveRequestsRaw, error: leaveRequestsError } = await db
.from('leave_requests')
.select(`
*,
leave_types (*),
staff!staff_id (first_name, last_name),
approved_by_staff:staff!approved_by (first_name, last_name)
`)
.in('staff_id', staffIds)
.eq('company_id', companyId)
.lte('start_date', dateRange.end_date)
.gte('end_date', dateRange.start_date)

Sorting Results

Reports are sorted by total leave days (highest first):

// Source: src/services/reportingService.ts:1023-1027
const sorted = leaveData.sort((a, b) => {
const totalA = Object.values(a.leave_taken_by_type).reduce((sum, days) => sum + days, 0)
const totalB = Object.values(b.leave_taken_by_type).reduce((sum, days) => sum + days, 0)
return totalB - totalA
})

Performance Metrics Integration

Leave in Staff Performance

Leave data is also included in staff performance metrics:

// Source: src/types/reporting.ts:82-84
metrics: {
// ...
leave_days_taken: number
// ...
}

This allows managers to see leave usage alongside other performance indicators.


Chart Data Generation

Visual Analytics

Leave data can be visualised with charts:

// Source: src/components/reporting/ReportingDashboard.tsx:225-226
const charts = ReportingService.generateChartData(result.data, 'leave_analysis')
setChartData(prev => ({ ...prev, leave: charts }))

Potential Chart Types

ChartShows
Bar ChartLeave days by staff member
Pie ChartLeave breakdown by type
Line ChartLeave trends over time
Stacked BarLeave types per staff member

Report Caching

Cache Bypass

Reports can bypass cache for fresh data:

// Source: src/components/reporting/ReportingDashboard.tsx:890
<ReportErrorBoundary
title="Leave analysis failed to render"
onRetry={() => fetchReportData({ cacheBypass: true })}
>

When to Bypass Cache

ScenarioUse Cache Bypass
After approving leave✅ Yes
After date range change❌ No (auto-refresh)
Suspected stale data✅ Yes
Regular viewing❌ No

Error Handling

Error Boundary

// Source: src/components/reporting/ReportingDashboard.tsx:889-940
<ReportErrorBoundary
title="Leave analysis failed to render"
onRetry={() => fetchReportData({ cacheBypass: true })}
>
{/* Report content */}
</ReportErrorBoundary>

Error Logging

// Source: src/services/reportingService.ts:1031
logger.error('Failed to generate leave analysis report', error as Error, {
service: 'reportingService',
method: 'generateLeaveAnalysisReport'
})

Best Practices

Report Generation

  1. Choose appropriate date range - Longer ranges = more data to process
  2. Use filters - Narrow down to relevant staff or leave types
  3. Export regularly - Keep historical records
  4. Review trends - Look for patterns in leave usage

Data Analysis

  1. Compare periods - Year-on-year or quarter-on-quarter
  2. Identify high usage - Staff approaching limits
  3. Monitor pending - Requests awaiting approval
  4. Track rejections - May indicate policy issues

Compliance

  1. Track entitlements - Ensure staff take minimum leave
  2. Monitor balances - Identify staff not using leave
  3. Year-end review - Check carry-over limits
  4. Audit trail - Export for records retention

Troubleshooting

Common Issues

IssueCauseSolution
No data showingDate range too narrowExpand date range
Missing staffFilter appliedClear staff filters
Export failsLarge datasetReduce date range or filter
Slow loadingMany recordsUse pagination, reduce page size

Performance Tips

TipBenefit
Use filtersReduces data volume
Smaller date rangesFaster queries
Paginate resultsBetter responsiveness
Cache bypass sparinglyReduces server load


Source Files:

  • src/types/reporting.ts:306-335 - LeaveAnalysisData interface
  • src/services/reportingService.ts:884-1033 - Leave report generation
  • src/components/reporting/ReportingDashboard.tsx:217-233 - Leave data fetching
  • src/components/reporting/ReportingDashboard.tsx:889-940 - Leave report display
  • src/app/api/reports/run/route.ts - Report API endpoint