Skip to main content

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:

ReportDescription
Hours SummaryTotal hours by staff, period, or department
Attendance ReportClock-in/out patterns and punctuality
Overtime ReportHours exceeding regular threshold
Break AnalysisBreak duration and compliance
Timesheet StatusSubmission 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

MetricCalculation
Total HoursSum of all total_hours
Regular HoursHours up to 40 per week
Overtime HoursHours exceeding 40 per week
Average DailyTotal hours / working days
HeadcountUnique 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

MetricDescription
On Time RatePercentage of punctual clock-ins
Late ArrivalsCount of late clock-ins
Average Late TimeMean minutes late when late
Early DeparturesClock-outs before shift end
Missing EntriesScheduled 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

ConfigurationValueDescription
Weekly Threshold40 hoursRegular hours limit
Daily Threshold8 hoursDaily overtime trigger
Overtime Rate1.5xStandard overtime multiplier
Double Time2.0xHoliday/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 LengthMinimum Break
Under 6 hoursNo mandatory break
6+ hours20 minutes (uninterrupted)
8+ hours20+ minutes (recommended)

Break Statistics

MetricDescription
Average Break DurationMean break time per shift
Compliance RatePercentage meeting minimum
Break Count AverageAverage breaks per shift
Missing BreaksShifts 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

StatusDescription
PendingSubmitted, awaiting review
ApprovedManager approved
RejectedSent back for correction

Processing Metrics

MetricDescription
Pending CountSubmissions awaiting review
Approval RatePercentage approved vs rejected
Avg TurnaroundDays from submission to review
Oldest PendingLongest 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

MetricUpdate Frequency
Currently Clocked InReal-time
On BreakReal-time
Hours This WeekDaily
Pending TimesheetsHourly
Overtime AlertDaily

Exporting Reports

Export Formats

FormatUse Case
PDFPrintable reports with branding
CSVPayroll system import
ExcelAnalysis and manipulation
JSONAPI 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

FilterTypeDescription
Date RangeDateStart and end dates
StaffUUIDSpecific staff member
RoleStringFilter by job role
DepartmentStringFilter by department
StatusEnumEntry or submission status

Date Range Presets

PresetDate Range
TodayCurrent date
This WeekMonday to Sunday
Last WeekPrevious Monday to Sunday
This Month1st to last day of month
Last MonthPrevious month
CustomUser-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

  1. Regular Reviews - Check reports weekly
  2. Trend Monitoring - Look for patterns over time
  3. Early Intervention - Address issues promptly
  4. Staff Feedback - Discuss findings with staff

For Data Accuracy

  1. Verify Entries - Check unusual data points
  2. Cross-Reference - Compare with schedules
  3. Audit Corrections - Review manual entries
  4. Document Anomalies - Record explanations

For Compliance

  1. Retain Records - Keep reports for required period
  2. Working Time Regulations - Monitor maximum hours
  3. Break Compliance - Ensure legal minimums
  4. Overtime Limits - Track opt-out agreements

Troubleshooting

Common Issues

IssueCauseSolution
Missing dataEntries not completedCheck for active clock-ins
Wrong totalsCalculation errorVerify individual entries
Export failsLarge datasetUse date filters
Slow loadingToo much dataReduce date range

Data Discrepancies

DiscrepancyCheck
Hours don't match scheduleManual entries or corrections
Staff missing from reportNo completed entries in period
Overtime incorrectWeekly threshold setting
Break data emptyStaff not using break feature


Source Files:

  • src/app/api/reports/time-tracking/route.ts - Time tracking reports API
  • src/lib/reports/time-tracking.ts - Report generation functions
  • src/components/reports/TimeTrackingDashboard.tsx - Dashboard component
  • src/types/database.types.ts:3161-3394 - Time entries and submissions schema