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:
- Submission Period - Weekly or fortnightly date range
- Time Entries - Individual clock-in/out records within the period
- Total Hours - Calculated sum of regular and overtime hours
- 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
Navigation
- From dashboard, click Time Tracking in sidebar
- Select Timesheets or Submissions
- View list of submitted timesheets
Timesheet List View
The timesheet list displays:
| Column | Description |
|---|---|
| Staff Name | Employee who submitted |
| Period | Start date - End date |
| Total Hours | Hours worked in period |
| Entries | Number of time entries |
| Status | Pending / Approved / Rejected |
| Submitted | When submitted |
| Actions | View, 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:
-
Header Information
- Staff name and role
- Submission period dates
- Current status badge
- Submission timestamp
-
Hours Summary
- Total hours worked
- Regular hours (up to threshold)
- Overtime hours (above threshold)
- Number of entries
-
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
- Review submission details and time entries
- Verify hours are accurate
- Check for any anomalies or missing entries
- Click Approve button
- 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
- Review submission details
- Identify issues requiring correction
- Click Reject button
- Enter rejection reason (required)
- 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
| Reason | Action Required |
|---|---|
| Missing entries | Staff to add missing clock-ins |
| Incorrect hours | Staff to correct entry times |
| Break not recorded | Staff to add break information |
| Exceeds schedule | Manager to verify overtime |
| Duplicate entries | Staff 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
| Format | Description |
|---|---|
| Formatted document with branding | |
| CSV | Raw data for payroll systems |
| Send 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
| Filter | Options |
|---|---|
| Status | All, Pending, Approved, Rejected |
| Staff | Individual staff member |
| Date Range | Custom start and end dates |
| Period | This 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
- Regular Reviews - Process submissions weekly
- Check Anomalies - Unusual hours or patterns
- Verify Overtime - Confirm overtime was authorised
- Document Rejections - Clear reasons help staff correct
For Efficient Processing
- Sort by Status - Review pending first
- Bulk Actions - Approve multiple when valid
- Set Deadlines - Staff submission windows
- Automate Reminders - For pending approvals
Troubleshooting
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Empty submission | No completed entries | Staff needs to clock out first |
| Wrong hours | Calculation error | Check individual entries |
| Cannot approve | Already processed | Submission already approved/rejected |
| Export fails | Missing data | Check company/staff details |
Error Messages
| Error | Meaning | Action |
|---|---|---|
| "No entries found" | No completed entries in period | Wait for staff to complete shifts |
| "Period exceeds 14 days" | Too long submission period | Use shorter date range |
| "Submission already exists" | Duplicate submission attempt | Check existing submissions |
| "Approval failed" | Database error | Retry or contact support |
Related Documentation
- Time Tracking Overview - Complete time tracking guide
- Time Corrections - Manual entry corrections
- Time Reports - Analytics and reporting
Source Files:
src/types/database.types.ts:3329-3394- Timesheet submissions schemasrc/app/api/staff/timesheet/submit/route.ts- Submission APIsrc/app/api/staff/timesheet/export/route.ts- PDF export APIsrc/app/api/timesheet/[id]/approve/route.ts- Approval APIsrc/app/api/timesheet/[id]/reject/route.ts- Rejection API