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 Dashboard → Reports
  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