Generating Reports
This guide covers how to generate, configure, and run reports using the Shyfts reporting dashboard and API.
Overview
Accessing Reports
- Navigate to Dashboard → Reports from the sidebar
- The Reports & Analytics dashboard loads automatically
- Select a report type from the tab navigation
- Configure date range and filters
- Click Refresh to run the report
Report Dashboard Interface
// Source: src/components/reporting/ReportingDashboard.tsx:46-55
const [activeTab, setActiveTab] = useState<
'overview' | 'performance' | 'scheduling' | 'attendance' | 'skills' | 'hours' | 'leave' | 'cost' | 'compliance'
>('overview')
const [dateRange, setDateRange] = useState<DateRange>({
start_date: moment.tz(UK_TIMEZONE).subtract(30, 'days').format('YYYY-MM-DD'),
end_date: moment.tz(UK_TIMEZONE).format('YYYY-MM-DD'),
period_type: 'MONTH'
})
Date Range Selection
Default Date Range
By default, reports cover the last 30 days:
- Start Date: 30 days ago
- End Date: Today
- Period Type: MONTH
Date Range Options
// Source: src/types/reporting.ts:27-33
interface DateRange {
start_date: string // YYYY-MM-DD format
end_date: string // YYYY-MM-DD format
period_type: 'DAY' | 'WEEK' | 'MONTH' | 'QUARTER' | 'YEAR' | 'CUSTOM'
}
Period Types
| Period | Description | Typical Use |
|---|---|---|
| DAY | Single day report | Daily snapshots |
| WEEK | 7-day period | Weekly summaries |
| MONTH | Calendar month | Monthly reviews |
| QUARTER | 3-month period | Quarterly analysis |
| YEAR | Full year | Annual reports |
| CUSTOM | User-defined | Flexible date ranges |
Using the Date Picker
The DateRangeSelector component provides:
- Quick presets - This week, Last week, This month, etc.
- Custom range - Select specific start and end dates
- Period display - Shows selected range in header
// Header display example
<p className="text-secondary-text mt-1">
{moment.tz(dateRange.start_date, 'YYYY-MM-DD', UK_TIMEZONE).format('D MMM YYYY')} -
{moment.tz(dateRange.end_date, 'YYYY-MM-DD', UK_TIMEZONE).format('D MMM YYYY')}
</p>
Filtering Reports
Available Filters
// Source: src/types/reporting.ts:35-45
interface ReportFilters {
staff_ids?: string[] // Specific staff members
role_ids?: string[] // Filter by roles
room_ids?: string[] // Filter by rooms
shift_types?: string[] // Shift type filter
shift_statuses?: string[] // Shift status filter
employment_types?: string[] // Full-time, part-time, etc.
leave_type_ids?: string[] // Leave types
time_entry_statuses?: string[] // Time entry states
skills?: string[] // Skill filters
}
Opening Filters Panel
// Source: src/components/reporting/ReportingDashboard.tsx:573-585
<button
onClick={() => setShowFiltersPanel(true)}
className="form-button secondary flex items-center gap-2"
>
<Filter className="w-4 h-4" />
Filters
{Object.keys(filters).filter(key => filters[key]?.length).length > 0 && (
<span className="ml-1 px-2 py-0.5 rounded-full bg-accent-coral text-xs font-semibold">
{activeFilterCount}
</span>
)}
</button>
Filter Examples
| Filter | Example Values | Effect |
|---|---|---|
| Staff IDs | ['staff-uuid-1', 'staff-uuid-2'] | Report only these staff |
| Role IDs | ['role-nurse', 'role-doctor'] | Filter by specific roles |
| Room IDs | ['room-1', 'room-2'] | Show only selected rooms |
| Employment Types | ['full-time', 'part-time'] | Filter by contract type |
Clearing Filters
- Click the filter badge to open the panel
- Use "Clear All" to reset filters
- Individual filters can be deselected
Running Reports
Manual Refresh
Click the Refresh button to re-run the current report:
// Source: src/components/reporting/ReportingDashboard.tsx:602-610
<button
onClick={() => fetchReportData({ cacheBypass: true })}
disabled={loading}
className="form-button flex items-center gap-2"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
Refresh
</button>
Cache Bypass
Use cacheBypass: true to force fresh data:
// Fresh data request
fetchReportData({ cacheBypass: true })
// Cached data (faster)
fetchReportData()
Automatic Updates
Reports automatically refresh when:
- Date range changes - New period selected
- Filters change - Filter criteria updated
- Tab switches - Different report type selected
// Source: src/components/reporting/ReportingDashboard.tsx:324-327
useEffect(() => {
fetchReportData()
}, [fetchReportData, dateRange, filters])
Live Updates
Enabling Live Mode
Toggle live updates for real-time data:
// Source: src/components/reporting/ReportingDashboard.tsx:594-601
<button
onClick={() => setLiveEnabled((v) => !v)}
className="form-button secondary"
disabled={!user?.company_id}
title={!user?.company_id ? 'Company scope required for live updates' : undefined}
>
Live: {liveEnabled ? (liveStatus === 'live' ? 'On' : '…') : 'Off'}
</button>
Live Status Indicators
| Status | Display | Meaning |
|---|---|---|
| off | Live: Off | Not subscribed |
| connecting | Live: … | Establishing connection |
| live | Live: On | Receiving updates |
| error | Live: Error | Connection failed |
Real-time Subscriptions
// Source: src/components/reporting/ReportingDashboard.tsx:346-361
const tablesToWatch: Array<'time_entries' | 'shifts' | 'leave_requests'> =
activeTab === 'scheduling'
? ['shifts']
: activeTab === 'leave'
? ['leave_requests']
: activeTab === 'hours'
? ['time_entries', 'shifts']
: ['time_entries', 'shifts', 'leave_requests']
The system monitors relevant database tables and auto-refreshes when changes occur.
Report API
Endpoint
POST /api/reports/run
Request Schema
// Source: src/app/api/reports/run/route.ts:46-57
const reportRunSchema = z.object({
reportType: reportTypeSchema, // Required: Report type
dateRange: dateRangeSchema, // Required: Date range
filters: z.record(z.unknown()).optional(), // Optional: Filters
pagination: paginationSchema, // Optional: Page settings
options: z.object({
cacheBypass: z.boolean().optional() // Skip cache
}).optional(),
companyId: z.string().uuid().optional() // For System Admins
})
Example Request
// Generate staff performance report
const response = await fetch('/api/reports/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
reportType: 'STAFF_PERFORMANCE',
dateRange: {
start_date: '2025-01-01',
end_date: '2025-01-31',
period_type: 'MONTH'
},
filters: {
role_ids: ['role-uuid-1']
},
pagination: {
page: 1,
pageSize: 50
}
})
})
const data = await response.json()
Response Format
// Successful response
{
success: true,
data: [...], // Report data
metadata: {
requestId: 'uuid',
generatedAt: '2025-01-14T10:30:00Z',
parametersHash: 'sha256-hash',
cache: {
hit: false,
ttlSeconds: 300
},
timing: 245, // ms
version: 'reports-v1',
pagination: {
page: 1,
pageSize: 50,
total: 125,
totalPages: 3,
hasNext: true,
hasPrev: false
}
}
}
Pagination
Paginated Reports
These reports support pagination:
- Staff Performance
- Attendance Summary
- Hours Tracking
- Leave Analysis
Pagination Controls
// Source: src/components/reporting/ReportingDashboard.tsx:499-554
const renderPaginationControls = (input: {
label: string
pagination: PaginationMeta | null
page: number
pageSize: number
setPage: (page: number) => void
setPageSize: (pageSize: number) => void
}) => {
return (
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div className="text-sm text-secondary-text">
{input.label}: Showing {from}-{to} of {total} (Page {page} of {totalPages})
</div>
<div className="flex items-center gap-2 flex-wrap">
<select
value={pageSize}
onChange={(e) => {
input.setPage(1)
input.setPageSize(Number(e.target.value))
}}
className="px-3 py-2 rounded-lg bg-white/10 border border-white/20"
>
{[25, 50, 100, 200].map((size) => (
<option key={size} value={size}>{size}</option>
))}
</select>
<button disabled={!pagination?.hasPrev} onClick={() => setPage(page - 1)}>
Prev
</button>
<button disabled={!pagination?.hasNext} onClick={() => setPage(page + 1)}>
Next
</button>
</div>
</div>
)
}
Page Size Options
| Size | Use Case |
|---|---|
| 25 | Quick browsing |
| 50 | Default view |
| 100 | Detailed review |
| 200 | Maximum per page |
Report Caching
Cache Behaviour
Reports are cached for improved performance:
// Source: src/app/api/reports/run/route.ts:197-220
if (!cacheBypass) {
const cached = getCachedReport<{ data: unknown; pagination?: PaginationMeta }>(cacheKey)
if (cached) {
logger.info('Report cache hit', {
reportType: parsedBody.data.reportType,
ttlSeconds: cached.ttlSeconds,
durationMs: Date.now() - startedAt
})
return ApiResponses.success(cached.value.data, {
cache: { hit: true, ttlSeconds: cached.ttlSeconds }
})
}
}
Cache Key Generation
// Source: src/app/api/reports/run/route.ts:59-71
function hashParameters(value: unknown): string {
const json = JSON.stringify(value, (_key, val) => {
if (!val || typeof val !== 'object' || Array.isArray(val)) return val
const record = val as Record<string, unknown>
return Object.keys(record)
.sort()
.reduce<Record<string, unknown>>((acc, key) => {
acc[key] = record[key]
return acc
}, {})
})
return crypto.createHash('sha256').update(json).digest('hex')
}
Cache TTL
Default cache duration: 5 minutes (300 seconds)
To bypass cache:
- Click Refresh button
- Enable Live mode
- Use
cacheBypass: truein API calls
Error Handling
Error Display
// Source: src/components/reporting/ReportingDashboard.tsx:614-631
{reportError && (
<div className="card-glass p-4 rounded-xl border border-red-500/30 bg-red-500/10">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div>
<div className="text-white font-semibold">Report failed to load</div>
<div className="text-red-200 text-sm">
{reportError.message}
</div>
</div>
<button
onClick={() => fetchReportData({ cacheBypass: true })}
className="form-button secondary"
>
Retry
</button>
</div>
</div>
)}
Common Errors
| Error | Cause | Solution |
|---|---|---|
| Unauthorized | Session expired | Re-login |
| Forbidden | Insufficient permissions | Contact admin |
| Too many requests | Rate limit exceeded | Wait and retry |
| Invalid report request | Bad parameters | Check date format |
Rate Limiting
// Source: src/app/api/reports/run/route.ts:130-137
const rate = reportRunLimiter.checkLimit(`reports:${user.id}`)
if (!rate.allowed) {
return ApiResponses.tooManyRequests('Too many report requests, please wait a moment.', {
requestId,
resetTime: rate.resetTime,
remainingRequests: rate.remainingRequests
})
}
Report Tabs
Tab Navigation
// Source: src/components/reporting/ReportingDashboard.tsx:487-497
const tabs = [
{ id: 'overview', label: 'Overview', icon: BarChart3 },
{ id: 'performance', label: 'Performance', icon: TrendingUp },
{ id: 'scheduling', label: 'Scheduling', icon: Calendar },
{ id: 'attendance', label: 'Attendance', icon: Clock },
{ id: 'skills', label: 'Skills', icon: Users },
{ id: 'hours', label: 'Hours', icon: Clock },
{ id: 'leave', label: 'Leave', icon: Calendar },
{ id: 'cost', label: 'Cost', icon: DollarSign },
{ id: 'compliance', label: 'Compliance', icon: ShieldCheck }
] as const
Overview Tab
The Overview tab runs all 8 reports simultaneously:
// Source: src/components/reporting/ReportingDashboard.tsx:266-278
case 'overview':
await Promise.all([
fetchPerformanceData(options),
fetchSchedulingData(options),
fetchAttendanceData(options),
fetchSkillsData(options),
fetchHoursData(options),
fetchLeaveData(options),
fetchCostData(options),
fetchComplianceData(options)
])
break
Best Practices
For Optimal Performance
- Use filters - Narrow down data scope
- Reasonable date ranges - Avoid very long periods
- Appropriate page sizes - Balance detail vs speed
- Leverage caching - Avoid unnecessary refreshes
For Accurate Reports
- Verify date range - Check start/end dates are correct
- Review filters - Ensure filters match requirements
- Compare periods - Use consistent date ranges for comparisons
- Check data freshness - Note cache status
For Regular Reporting
| Frequency | Recommended Reports |
|---|---|
| Daily | Attendance, Hours |
| Weekly | Performance, Scheduling |
| Monthly | Cost, Leave, Compliance |
| Quarterly | All reports for review |
Related Documentation
- Available Reports - Report types and metrics
- Exporting Data - Export to PDF/CSV/Excel
- Time Tracking Reports - Detailed time analytics
Source Files:
src/components/reporting/ReportingDashboard.tsx- Dashboard componentsrc/app/api/reports/run/route.ts- Report execution APIsrc/types/reporting.ts- Type definitionssrc/lib/cache/reportCache.ts- Caching utilitiessrc/services/reportingService.ts- Report generation service