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