Skip to main content

Viewing Timesheets

This guide covers timesheet review, approval workflows, submission management, and PDF export functionality for Company Managers.


Overview

What are Timesheets?

Timesheets in Shyfts are periodic submissions that group time entries for review and approval:

  1. Submission Period - Weekly or fortnightly date range
  2. Time Entries - Individual clock-in/out records within the period
  3. Total Hours - Calculated sum of regular and overtime hours
  4. Status Tracking - Pending, approved, or rejected state

Timesheet Submissions Database Structure

// Source: src/types/database.types.ts:3329-3394
timesheet_submissions: {
Row: {
id: string
staff_id: string
company_id: string
start_date: string // Period start (e.g., Monday)
end_date: string // Period end (e.g., Sunday)
total_hours: number // Sum of all hours
regular_hours: number // Standard working hours
overtime_hours: number // Hours above regular threshold
total_entries: number // Count of time entries
status: string // pending, approved, rejected
submitted_at: string // When staff submitted
submitted_by: string // User who submitted
reviewed_at: string | null // When manager reviewed
reviewed_by: string | null // Manager who reviewed
rejection_reason: string | null // If rejected, why
created_at: string
updated_at: string
}
}

Accessing Timesheets

  1. From dashboard, click Time Tracking in sidebar
  2. Select Timesheets or Submissions
  3. View list of submitted timesheets

Timesheet List View

The timesheet list displays:

ColumnDescription
Staff NameEmployee who submitted
PeriodStart date - End date
Total HoursHours worked in period
EntriesNumber of time entries
StatusPending / Approved / Rejected
SubmittedWhen submitted
ActionsView, Approve, Reject buttons

Submission Workflow

Staff Submission Process

// Source: src/app/api/staff/timesheet/submit/route.ts:25-75
export async function POST(request: NextRequest) {
const supabase = await createClient()
const body = await request.json()

const { startDate, endDate } = body

// Validate 14-day maximum period
const daysDiff = differenceInDays(new Date(endDate), new Date(startDate))
if (daysDiff > 14) {
return NextResponse.json(
{ error: 'Submission period cannot exceed 14 days' },
{ status: 400 }
)
}

// Get time entries for period
const { data: entries } = await supabase
.from('time_entries')
.select('*')
.eq('staff_id', staffId)
.gte('clock_in_time', startDate)
.lte('clock_in_time', endDate)
.eq('status', 'completed')

// Calculate totals
const totalHours = entries.reduce((sum, e) => sum + e.total_hours, 0)
const regularHours = Math.min(totalHours, 40)
const overtimeHours = Math.max(0, totalHours - 40)

// Create submission using RPC
const { data: submission } = await supabase
.rpc('submit_timesheet', {
p_staff_id: staffId,
p_company_id: companyId,
p_start_date: startDate,
p_end_date: endDate,
p_total_hours: totalHours,
p_regular_hours: regularHours,
p_overtime_hours: overtimeHours,
p_total_entries: entries.length
})

// Update entry statuses
await supabase
.from('time_entries')
.update({ status: 'submitted', submission_id: submission.id })
.in('id', entries.map(e => e.id))

return NextResponse.json(submission)
}

Status Flow

Staff Submits      Manager Reviews     Result
│ │
▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Pending │ ───► │ Review │ ───► │ Approved │
└──────────┘ └──────────┘ └──────────┘


┌──────────┐
│ Rejected │
└──────────┘

Viewing Submission Details

Submission Detail View

When viewing a submission, you see:

  1. Header Information

    • Staff name and role
    • Submission period dates
    • Current status badge
    • Submission timestamp
  2. Hours Summary

    • Total hours worked
    • Regular hours (up to threshold)
    • Overtime hours (above threshold)
    • Number of entries
  3. Time Entries List

    • Each clock-in/out record
    • Date and times
    • Break duration
    • Entry status

Example Submission Data

{
"id": "submission-uuid",
"staff_id": "staff-uuid",
"staff": {
"first_name": "Sarah",
"last_name": "Johnson",
"email": "sarah@example.com"
},
"start_date": "2025-01-06",
"end_date": "2025-01-12",
"total_hours": 42.5,
"regular_hours": 40,
"overtime_hours": 2.5,
"total_entries": 5,
"status": "pending",
"submitted_at": "2025-01-13T09:00:00Z",
"entries": [
{
"date": "2025-01-06",
"clock_in": "09:00",
"clock_out": "17:30",
"breaks": "00:30",
"total": "8.0"
}
]
}

Approving Timesheets

Approval Process

  1. Review submission details and time entries
  2. Verify hours are accurate
  3. Check for any anomalies or missing entries
  4. Click Approve button
  5. Entries marked as approved

Approval API

Endpoint: POST /api/timesheet/[id]/approve

// Approval handler
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const supabase = await createClient()
const submissionId = params.id

// Update submission status
const { data: submission } = await supabase
.from('timesheet_submissions')
.update({
status: 'approved',
reviewed_at: new Date().toISOString(),
reviewed_by: userId
})
.eq('id', submissionId)
.select()
.single()

// Update all related entries
await supabase
.from('time_entries')
.update({
status: 'approved',
approved_at: new Date().toISOString(),
approved_by: userId
})
.eq('submission_id', submissionId)

return NextResponse.json(submission)
}

Bulk Approval

For approving multiple submissions:

const approveMultiple = async (submissionIds: string[]) => {
const promises = submissionIds.map(id =>
fetch(`/api/timesheet/${id}/approve`, { method: 'POST' })
)
await Promise.all(promises)
}

Rejecting Timesheets

Rejection Process

  1. Review submission details
  2. Identify issues requiring correction
  3. Click Reject button
  4. Enter rejection reason (required)
  5. Staff notified to correct and resubmit

Rejection API

Endpoint: POST /api/timesheet/[id]/reject

// Rejection handler
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const body = await request.json()
const { reason } = body

if (!reason) {
return NextResponse.json(
{ error: 'Rejection reason required' },
{ status: 400 }
)
}

// Update submission status
const { data: submission } = await supabase
.from('timesheet_submissions')
.update({
status: 'rejected',
reviewed_at: new Date().toISOString(),
reviewed_by: userId,
rejection_reason: reason
})
.eq('id', submissionId)
.select()
.single()

// Reset entry statuses for resubmission
await supabase
.from('time_entries')
.update({ status: 'completed', submission_id: null })
.eq('submission_id', submissionId)

return NextResponse.json(submission)
}

Common Rejection Reasons

ReasonAction Required
Missing entriesStaff to add missing clock-ins
Incorrect hoursStaff to correct entry times
Break not recordedStaff to add break information
Exceeds scheduleManager to verify overtime
Duplicate entriesStaff to remove duplicates

Exporting Timesheets

PDF Export

Generate PDF timesheets for records or payroll:

// Source: src/app/api/staff/timesheet/export/route.ts:45-180
export async function POST(request: NextRequest) {
const body = await request.json()
const { submissionId, format = 'pdf' } = body

// Get submission with entries
const { data: submission } = await supabase
.from('timesheet_submissions')
.select(`
*,
staff:staff_id (first_name, last_name, email, job_title),
entries:time_entries (*)
`)
.eq('id', submissionId)
.single()

// Get company details for branding
const { data: company } = await supabase
.from('companies')
.select('name, logo_url, address')
.eq('id', submission.company_id)
.single()

// Generate PDF using jsPDF
const doc = new jsPDF()

// Company header
doc.setFontSize(20)
doc.text(company.name, 20, 20)
if (company.logo_url) {
doc.addImage(company.logo_url, 'PNG', 150, 10, 40, 20)
}

// Staff details
doc.setFontSize(12)
doc.text(`Employee: ${submission.staff.first_name} ${submission.staff.last_name}`, 20, 40)
doc.text(`Period: ${formatDate(submission.start_date)} - ${formatDate(submission.end_date)}`, 20, 50)

// Hours summary
doc.text(`Total Hours: ${submission.total_hours}`, 20, 70)
doc.text(`Regular Hours: ${submission.regular_hours}`, 20, 80)
doc.text(`Overtime Hours: ${submission.overtime_hours}`, 20, 90)

// Time entries table
const tableData = submission.entries.map(entry => [
formatDate(entry.work_date),
formatTime(entry.clock_in_time),
formatTime(entry.clock_out_time),
formatDuration(entry.break_duration),
entry.total_hours.toFixed(2)
])

doc.autoTable({
head: [['Date', 'Clock In', 'Clock Out', 'Breaks', 'Hours']],
body: tableData,
startY: 100
})

// Signature lines
const finalY = doc.lastAutoTable.finalY + 30
doc.text('Employee Signature: _________________', 20, finalY)
doc.text('Manager Signature: _________________', 20, finalY + 15)
doc.text('Date: _________________', 20, finalY + 30)

// Return PDF
const pdfBuffer = doc.output('arraybuffer')
return new Response(pdfBuffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="timesheet-${submission.id}.pdf"`
}
})
}

Export Options

FormatDescription
PDFFormatted document with branding
CSVRaw data for payroll systems
EmailSend PDF as attachment

Email Delivery

// Source: src/app/api/staff/timesheet/export/route.ts:220-280
if (body.sendEmail) {
const pdfBuffer = doc.output('arraybuffer')

await resend.emails.send({
from: 'timesheets@cflow.app',
to: submission.staff.email,
subject: `Timesheet ${formatDate(submission.start_date)} - ${formatDate(submission.end_date)}`,
attachments: [{
filename: `timesheet-${submission.id}.pdf`,
content: Buffer.from(pdfBuffer)
}],
html: `
<h2>Your Timesheet</h2>
<p>Period: ${formatDate(submission.start_date)} - ${formatDate(submission.end_date)}</p>
<p>Total Hours: ${submission.total_hours}</p>
<p>Status: ${submission.status}</p>
`
})
}

Filtering and Searching

Filter Options

FilterOptions
StatusAll, Pending, Approved, Rejected
StaffIndividual staff member
Date RangeCustom start and end dates
PeriodThis week, Last week, This month

Search Functionality

// Filter timesheets
const filterSubmissions = (submissions, filters) => {
return submissions.filter(s => {
// Status filter
if (filters.status && s.status !== filters.status) return false

// Staff filter
if (filters.staffId && s.staff_id !== filters.staffId) return false

// Date range filter
if (filters.startDate && s.start_date < filters.startDate) return false
if (filters.endDate && s.end_date > filters.endDate) return false

return true
})
}

Automatic Hours Submissions

For Non-Clock Staff

Staff with requires_clock_in = false have entries auto-generated:

// Source: src/app/api/staff/timesheet/submit/route.ts:185-250
// Auto-generate entries from availability
const { data: availability } = await supabase
.from('staff_availability')
.select('*')
.eq('staff_id', staffId)
.gte('date', startDate)
.lte('date', endDate)

const autoEntries = availability.map(slot => ({
staff_id: staffId,
company_id: companyId,
clock_in_time: `${slot.date}T${slot.start_time}`,
clock_out_time: `${slot.date}T${slot.end_time}`,
total_hours: calculateHours(slot.start_time, slot.end_time),
entry_type: 'automatic',
status: 'submitted',
submission_id: submissionId
}))

await supabase.from('time_entries').insert(autoEntries)

Best Practices

For Reviewing Timesheets

  1. Regular Reviews - Process submissions weekly
  2. Check Anomalies - Unusual hours or patterns
  3. Verify Overtime - Confirm overtime was authorised
  4. Document Rejections - Clear reasons help staff correct

For Efficient Processing

  1. Sort by Status - Review pending first
  2. Bulk Actions - Approve multiple when valid
  3. Set Deadlines - Staff submission windows
  4. Automate Reminders - For pending approvals

Troubleshooting

Common Issues

IssueCauseSolution
Empty submissionNo completed entriesStaff needs to clock out first
Wrong hoursCalculation errorCheck individual entries
Cannot approveAlready processedSubmission already approved/rejected
Export failsMissing dataCheck company/staff details

Error Messages

ErrorMeaningAction
"No entries found"No completed entries in periodWait for staff to complete shifts
"Period exceeds 14 days"Too long submission periodUse shorter date range
"Submission already exists"Duplicate submission attemptCheck existing submissions
"Approval failed"Database errorRetry or contact support


Source Files:

  • src/types/database.types.ts:3329-3394 - Timesheet submissions schema
  • src/app/api/staff/timesheet/submit/route.ts - Submission API
  • src/app/api/staff/timesheet/export/route.ts - PDF export API
  • src/app/api/timesheet/[id]/approve/route.ts - Approval API
  • src/app/api/timesheet/[id]/reject/route.ts - Rejection API