Time Reports
This guide covers time tracking reports, analytics dashboards, and data export capabilities for Company Managers.
Overview
Available Time Reports
Shyfts provides comprehensive time tracking analytics:
| Report | Description |
|---|---|
| Hours Summary | Total hours by staff, period, or department |
| Attendance Report | Clock-in/out patterns and punctuality |
| Overtime Report | Hours exceeding regular threshold |
| Break Analysis | Break duration and compliance |
| Timesheet Status | Submission and approval tracking |
Hours Summary Report
Report Contents
The Hours Summary provides aggregated working hours data:
// Hours summary query
const getHoursSummary = async (params: {
companyId: string
startDate: string
endDate: string
groupBy?: 'staff' | 'day' | 'week' | 'role'
}) => {
const { data } = await supabase
.from('time_entries')
.select(`
staff_id,
staff:staff_id (first_name, last_name, job_title),
total_hours,
work_date,
entry_type
`)
.eq('company_id', params.companyId)
.gte('work_date', params.startDate)
.lte('work_date', params.endDate)
.in('status', ['completed', 'approved'])
// Group and aggregate
const grouped = groupByField(data, params.groupBy || 'staff')
return calculateTotals(grouped)
}
Summary Metrics
| Metric | Calculation |
|---|---|
| Total Hours | Sum of all total_hours |
| Regular Hours | Hours up to 40 per week |
| Overtime Hours | Hours exceeding 40 per week |
| Average Daily | Total hours / working days |
| Headcount | Unique staff with entries |
Example Output
{
"period": {
"start": "2025-01-06",
"end": "2025-01-12"
},
"summary": {
"totalHours": 452.5,
"regularHours": 400,
"overtimeHours": 52.5,
"averagePerStaff": 37.7,
"staffCount": 12
},
"byStaff": [
{
"staffId": "staff-1",
"name": "Sarah Johnson",
"totalHours": 42.5,
"regularHours": 40,
"overtimeHours": 2.5
}
]
}
Attendance Report
Tracking Attendance Patterns
The attendance report analyses clock-in/out behaviour:
// Attendance analysis
const getAttendanceReport = async (params: {
companyId: string
startDate: string
endDate: string
}) => {
const { data: entries } = await supabase
.from('time_entries')
.select(`
id,
staff_id,
staff:staff_id (first_name, last_name, shift_start_time),
clock_in_time,
clock_out_time,
work_date,
location_data
`)
.eq('company_id', params.companyId)
.gte('work_date', params.startDate)
.lte('work_date', params.endDate)
// Calculate attendance metrics
const metrics = entries.map(entry => {
const clockIn = new Date(entry.clock_in_time)
const scheduledStart = parseScheduledTime(entry.staff.shift_start_time, entry.work_date)
return {
...entry,
isLate: clockIn > scheduledStart,
lateMinutes: clockIn > scheduledStart
? Math.round((clockIn - scheduledStart) / 60000)
: 0,
hasLocation: !!entry.location_data
}
})
return metrics
}
Attendance Metrics
| Metric | Description |
|---|---|
| On Time Rate | Percentage of punctual clock-ins |
| Late Arrivals | Count of late clock-ins |
| Average Late Time | Mean minutes late when late |
| Early Departures | Clock-outs before shift end |
| Missing Entries | Scheduled but no clock-in |
Punctuality Analysis
// Calculate punctuality statistics
const calculatePunctuality = (entries) => {
const total = entries.length
const onTime = entries.filter(e => !e.isLate).length
const late = entries.filter(e => e.isLate)
return {
onTimeRate: Math.round((onTime / total) * 100),
lateCount: late.length,
averageLateMinutes: late.length > 0
? Math.round(late.reduce((sum, e) => sum + e.lateMinutes, 0) / late.length)
: 0,
mostFrequentLate: getMostFrequentLateStaff(late)
}
}
Overtime Report
Tracking Overtime Hours
Overtime analysis helps manage labour costs:
// Overtime calculation
const getOvertimeReport = async (params: {
companyId: string
startDate: string
endDate: string
threshold?: number // Default 40 hours/week
}) => {
const threshold = params.threshold || 40
// Get weekly totals per staff
const { data: entries } = await supabase
.from('time_entries')
.select(`
staff_id,
staff:staff_id (first_name, last_name, hourly_rate),
total_hours,
work_date
`)
.eq('company_id', params.companyId)
.gte('work_date', params.startDate)
.lte('work_date', params.endDate)
// Group by week and calculate overtime
const weeklyData = groupByWeek(entries)
const overtimeData = weeklyData.map(week => {
const totalHours = week.entries.reduce((sum, e) => sum + e.total_hours, 0)
const overtimeHours = Math.max(0, totalHours - threshold)
return {
weekStart: week.startDate,
weekEnd: week.endDate,
staffId: week.staffId,
totalHours,
regularHours: Math.min(totalHours, threshold),
overtimeHours,
overtimeCost: overtimeHours * (week.hourlyRate * 1.5) // 1.5x rate
}
})
return overtimeData
}
Overtime Thresholds
| Configuration | Value | Description |
|---|---|---|
| Weekly Threshold | 40 hours | Regular hours limit |
| Daily Threshold | 8 hours | Daily overtime trigger |
| Overtime Rate | 1.5x | Standard overtime multiplier |
| Double Time | 2.0x | Holiday/excessive overtime |
Cost Analysis
// Calculate overtime costs
const calculateOvertimeCosts = (overtimeData) => {
const totalOvertimeHours = overtimeData.reduce((sum, d) => sum + d.overtimeHours, 0)
const totalOvertimeCost = overtimeData.reduce((sum, d) => sum + d.overtimeCost, 0)
return {
totalOvertimeHours,
totalOvertimeCost: totalOvertimeCost.toFixed(2),
averagePerStaff: (totalOvertimeHours / new Set(overtimeData.map(d => d.staffId)).size).toFixed(1),
staffWithOvertime: new Set(overtimeData.filter(d => d.overtimeHours > 0).map(d => d.staffId)).size
}
}
Break Analysis Report
Break Compliance Tracking
Monitor break patterns and compliance:
// Break analysis
const getBreakAnalysis = async (params: {
companyId: string
startDate: string
endDate: string
}) => {
const { data: entries } = await supabase
.from('time_entries')
.select(`
id,
staff_id,
staff:staff_id (first_name, last_name),
break_cycles,
break_duration,
total_hours,
work_date
`)
.eq('company_id', params.companyId)
.gte('work_date', params.startDate)
.lte('work_date', params.endDate)
.not('break_cycles', 'is', null)
// Analyse break patterns
const analysis = entries.map(entry => {
const breakCycles = entry.break_cycles || []
const totalBreakMinutes = entry.break_duration * 60
// UK minimum: 20 min for 6+ hour shifts
const requiredBreak = entry.total_hours >= 6 ? 20 : 0
const isCompliant = totalBreakMinutes >= requiredBreak
return {
...entry,
breakCount: breakCycles.length,
totalBreakMinutes: Math.round(totalBreakMinutes),
requiredBreakMinutes: requiredBreak,
isCompliant,
shortfall: Math.max(0, requiredBreak - totalBreakMinutes)
}
})
return analysis
}
Break Compliance Rules (UK)
| Shift Length | Minimum Break |
|---|---|
| Under 6 hours | No mandatory break |
| 6+ hours | 20 minutes (uninterrupted) |
| 8+ hours | 20+ minutes (recommended) |
Break Statistics
| Metric | Description |
|---|---|
| Average Break Duration | Mean break time per shift |
| Compliance Rate | Percentage meeting minimum |
| Break Count Average | Average breaks per shift |
| Missing Breaks | Shifts without logged breaks |
Timesheet Status Report
Submission Tracking
Monitor timesheet submission and approval:
// Timesheet status report
const getTimesheetStatusReport = async (params: {
companyId: string
startDate: string
endDate: string
}) => {
const { data: submissions } = await supabase
.from('timesheet_submissions')
.select(`
id,
staff_id,
staff:staff_id (first_name, last_name),
start_date,
end_date,
total_hours,
status,
submitted_at,
reviewed_at,
reviewed_by
`)
.eq('company_id', params.companyId)
.gte('start_date', params.startDate)
.lte('end_date', params.endDate)
// Group by status
const byStatus = {
pending: submissions.filter(s => s.status === 'pending'),
approved: submissions.filter(s => s.status === 'approved'),
rejected: submissions.filter(s => s.status === 'rejected')
}
// Calculate turnaround time
const approvedWithTurnaround = byStatus.approved.map(s => ({
...s,
turnaroundDays: differenceInDays(new Date(s.reviewed_at), new Date(s.submitted_at))
}))
return {
byStatus,
totals: {
pending: byStatus.pending.length,
approved: byStatus.approved.length,
rejected: byStatus.rejected.length,
total: submissions.length
},
averageTurnaround: calculateAverageTurnaround(approvedWithTurnaround)
}
}
Status Breakdown
| Status | Description |
|---|---|
| Pending | Submitted, awaiting review |
| Approved | Manager approved |
| Rejected | Sent back for correction |
Processing Metrics
| Metric | Description |
|---|---|
| Pending Count | Submissions awaiting review |
| Approval Rate | Percentage approved vs rejected |
| Avg Turnaround | Days from submission to review |
| Oldest Pending | Longest waiting submission |
Report Dashboard
Dashboard Components
// Time tracking dashboard data
const getTimeTrackingDashboard = async (companyId: string) => {
const today = new Date()
const weekStart = startOfWeek(today, { weekStartsOn: 1 })
const weekEnd = endOfWeek(today, { weekStartsOn: 1 })
const monthStart = startOfMonth(today)
const monthEnd = endOfMonth(today)
// Parallel queries for dashboard
const [todayStats, weekStats, monthStats, pendingTimesheets] = await Promise.all([
getTodayClockStats(companyId),
getHoursSummary({ companyId, startDate: weekStart, endDate: weekEnd }),
getHoursSummary({ companyId, startDate: monthStart, endDate: monthEnd }),
getPendingTimesheets(companyId)
])
return {
today: {
clockedIn: todayStats.clockedInCount,
onBreak: todayStats.onBreakCount,
clockedOut: todayStats.clockedOutCount
},
thisWeek: {
totalHours: weekStats.summary.totalHours,
overtimeHours: weekStats.summary.overtimeHours,
staffWorking: weekStats.summary.staffCount
},
thisMonth: {
totalHours: monthStats.summary.totalHours,
averagePerStaff: monthStats.summary.averagePerStaff
},
pending: {
timesheets: pendingTimesheets.length,
oldestDays: getOldestPendingDays(pendingTimesheets)
}
}
}
Key Metrics Display
| Metric | Update Frequency |
|---|---|
| Currently Clocked In | Real-time |
| On Break | Real-time |
| Hours This Week | Daily |
| Pending Timesheets | Hourly |
| Overtime Alert | Daily |
Exporting Reports
Export Formats
| Format | Use Case |
|---|---|
| Printable reports with branding | |
| CSV | Payroll system import |
| Excel | Analysis and manipulation |
| JSON | API integration |
Export API
Endpoint: POST /api/reports/time-tracking/export
export async function POST(request: NextRequest) {
const body = await request.json()
const { reportType, format, params } = body
// Generate report data
let reportData
switch (reportType) {
case 'hours-summary':
reportData = await getHoursSummary(params)
break
case 'attendance':
reportData = await getAttendanceReport(params)
break
case 'overtime':
reportData = await getOvertimeReport(params)
break
}
// Format output
switch (format) {
case 'csv':
return generateCSV(reportData)
case 'pdf':
return generatePDF(reportData, reportType)
case 'excel':
return generateExcel(reportData)
default:
return NextResponse.json(reportData)
}
}
CSV Export Example
// Generate CSV for hours summary
const generateHoursCSV = (data) => {
const headers = ['Staff Name', 'Job Title', 'Regular Hours', 'Overtime Hours', 'Total Hours']
const rows = data.byStaff.map(s => [
`${s.firstName} ${s.lastName}`,
s.jobTitle,
s.regularHours.toFixed(2),
s.overtimeHours.toFixed(2),
s.totalHours.toFixed(2)
])
return [headers, ...rows].map(row => row.join(',')).join('\n')
}
Filtering and Parameters
Common Filter Options
| Filter | Type | Description |
|---|---|---|
| Date Range | Date | Start and end dates |
| Staff | UUID | Specific staff member |
| Role | String | Filter by job role |
| Department | String | Filter by department |
| Status | Enum | Entry or submission status |
Date Range Presets
| Preset | Date Range |
|---|---|
| Today | Current date |
| This Week | Monday to Sunday |
| Last Week | Previous Monday to Sunday |
| This Month | 1st to last day of month |
| Last Month | Previous month |
| Custom | User-specified range |
Scheduling Reports
Automated Reports
Set up scheduled report delivery:
// Schedule configuration
const reportSchedule = {
weekly: {
dayOfWeek: 1, // Monday
time: '07:00',
reports: ['hours-summary', 'overtime'],
recipients: ['manager@company.com']
},
monthly: {
dayOfMonth: 1,
time: '08:00',
reports: ['attendance', 'break-analysis'],
recipients: ['hr@company.com', 'manager@company.com']
}
}
Email Delivery
// Send scheduled report
const sendScheduledReport = async (schedule) => {
const reportData = await generateReport(schedule.reportType, schedule.params)
const pdfBuffer = await generatePDF(reportData)
await resend.emails.send({
from: 'reports@cflow.app',
to: schedule.recipients,
subject: `${schedule.reportType} Report - ${formatDate(new Date())}`,
attachments: [{
filename: `${schedule.reportType}-${formatDate(new Date())}.pdf`,
content: Buffer.from(pdfBuffer)
}],
html: generateReportEmailHTML(schedule.reportType, reportData)
})
}
Best Practices
For Report Analysis
- Regular Reviews - Check reports weekly
- Trend Monitoring - Look for patterns over time
- Early Intervention - Address issues promptly
- Staff Feedback - Discuss findings with staff
For Data Accuracy
- Verify Entries - Check unusual data points
- Cross-Reference - Compare with schedules
- Audit Corrections - Review manual entries
- Document Anomalies - Record explanations
For Compliance
- Retain Records - Keep reports for required period
- Working Time Regulations - Monitor maximum hours
- Break Compliance - Ensure legal minimums
- Overtime Limits - Track opt-out agreements
Troubleshooting
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Missing data | Entries not completed | Check for active clock-ins |
| Wrong totals | Calculation error | Verify individual entries |
| Export fails | Large dataset | Use date filters |
| Slow loading | Too much data | Reduce date range |
Data Discrepancies
| Discrepancy | Check |
|---|---|
| Hours don't match schedule | Manual entries or corrections |
| Staff missing from report | No completed entries in period |
| Overtime incorrect | Weekly threshold setting |
| Break data empty | Staff not using break feature |
Related Documentation
- Time Tracking Overview - Complete time tracking guide
- Viewing Timesheets - Timesheet management
- Time Corrections - Manual entry corrections
- Available Reports - All report types
Source Files:
src/app/api/reports/time-tracking/route.ts- Time tracking reports APIsrc/lib/reports/time-tracking.ts- Report generation functionssrc/components/reports/TimeTrackingDashboard.tsx- Dashboard componentsrc/types/database.types.ts:3161-3394- Time entries and submissions schema