Skip to main content

Viewing Your Timesheet

This guide covers how to view, filter, and review your timesheet entries, including understanding the different views, statuses, and how to identify issues before submission.


Overview

What is the Timesheet View?

The timesheet view displays all your recorded time entries:

  1. Daily View - Entries for a specific day
  2. Weekly View - Summary by week (Monday-Sunday)
  3. Period View - Custom date range selection
  4. Status Filtering - Filter by entry status

Accessing Your Timesheet

  1. Navigate to Staff PortalTimesheet from the sidebar
  2. The current week's entries load by default
  3. Use filters to adjust the view

Timesheet Query API

Fetch Timesheet Entries

Endpoint: GET /api/staff/timesheet

// Source: src/app/api/staff/timesheet/route.ts:28-65
const timesheetQuerySchema = z.object({
start_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Invalid date format. Use YYYY-MM-DD'),
end_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Invalid date format. Use YYYY-MM-DD'),
status: z.enum(['draft', 'submitted', 'approved', 'rejected']).optional(),
include_totals: z.enum(['true', 'false']).optional().default('true')
})

Query Parameters

ParameterTypeRequiredDescription
start_datestringYesStart of date range (YYYY-MM-DD)
end_datestringYesEnd of date range (YYYY-MM-DD)
statusstringNoFilter by entry status
include_totalsbooleanNoInclude summary totals (default: true)

Example Request

// Fetch this week's timesheet
const startDate = moment().startOf('isoWeek').format('YYYY-MM-DD')
const endDate = moment().endOf('isoWeek').format('YYYY-MM-DD')

const response = await fetch(
`/api/staff/timesheet?start_date=${startDate}&end_date=${endDate}&include_totals=true`
)

const data = await response.json()

Response Format

// Timesheet response structure
{
success: true,
data: {
entries: [
{
id: "entry-uuid-1",
staff_id: "staff-uuid",
clock_in_time: "2025-01-13T09:00:00Z",
clock_out_time: "2025-01-13T17:30:00Z",
break_duration: 30,
total_hours: 8.0,
status: "draft",
notes: "Standard shift",
shift_id: "shift-uuid",
created_at: "2025-01-13T09:00:00Z"
},
// ... more entries
],
totals: {
total_hours: 40.0,
total_entries: 5,
draft_count: 2,
submitted_count: 3,
approved_count: 0,
rejected_count: 0
},
period: {
start_date: "2025-01-13",
end_date: "2025-01-19"
}
}
}

Entry Status Understanding

Status Definitions

StatusIconColourDescription
DraftPencilGreyEntry created, not submitted
SubmittedClockBlueAwaiting manager approval
ApprovedCheckGreenConfirmed by manager
RejectedXRedRequires attention/correction

Status Workflow

Draft → Submitted → Approved
↘ Rejected → (Edit) → Submitted

Filtering by Status

// Filter entries by status
const getDraftEntries = async () => {
const response = await fetch(
`/api/staff/timesheet?start_date=${startDate}&end_date=${endDate}&status=draft`
)
return response.json()
}

const getRejectedEntries = async () => {
const response = await fetch(
`/api/staff/timesheet?start_date=${startDate}&end_date=${endDate}&status=rejected`
)
return response.json()
}

Daily View

Viewing a Specific Day

// Fetch entries for a specific day
const viewDay = async (date: string) => {
const response = await fetch(
`/api/staff/timesheet?start_date=${date}&end_date=${date}`
)
return response.json()
}

Daily Entry Display

ColumnDescriptionFormat
DateEntry dateDD/MM/YYYY
Clock InStart timeHH:MM
Clock OutEnd timeHH:MM
BreakBreak durationMinutes
TotalNet working hoursX.XX hrs
StatusEntry statusBadge
ActionsEdit/View optionsButtons

Daily Summary Card

// Daily summary calculation
const dailySummary = {
date: '14/01/2025',
entries: 1,
totalHours: 8.0,
breakMinutes: 30,
status: 'draft',
scheduledHours: 8.5,
variance: -0.5 // Actual vs scheduled
}

Weekly View

Week Structure (UK Standard)

Weeks run Monday to Sunday (UK business week):

DayPosition
MondayWeek start
TuesdayDay 2
WednesdayDay 3
ThursdayDay 4
FridayDay 5
SaturdayDay 6
SundayWeek end

Weekly Summary Display

// Weekly summary structure
interface WeeklySummary {
weekNumber: number
startDate: string // Monday
endDate: string // Sunday
totalHours: number
scheduledHours: number
variance: number
entries: TimeEntry[]
statusBreakdown: {
draft: number
submitted: number
approved: number
rejected: number
}
}

Weekly Totals Calculation

// Source: src/app/api/staff/timesheet/route.ts:145-180
const calculateWeeklyTotals = (entries: TimeEntry[]) => {
const totals = {
total_hours: 0,
total_entries: entries.length,
draft_count: 0,
submitted_count: 0,
approved_count: 0,
rejected_count: 0
}

entries.forEach(entry => {
totals.total_hours += entry.total_hours || 0

switch (entry.status) {
case 'draft': totals.draft_count++; break
case 'submitted': totals.submitted_count++; break
case 'approved': totals.approved_count++; break
case 'rejected': totals.rejected_count++; break
}
})

return totals
}

Period Selection

Date Range Picker

The timesheet includes a date range selector:

// Date range selection component
interface DateRange {
startDate: string // YYYY-MM-DD
endDate: string // YYYY-MM-DD
}

// Quick presets
const presets = [
{ label: 'This Week', range: getCurrentWeek() },
{ label: 'Last Week', range: getLastWeek() },
{ label: 'This Month', range: getCurrentMonth() },
{ label: 'Last Month', range: getLastMonth() },
{ label: 'Custom', range: null } // Opens date picker
]

UK Date Format Display

All dates displayed in UK format:

FormatExampleUse
DD/MM/YYYY14/01/2025Standard display
ddd DD MMMMon 14 JanShort format
dddd, D MMMM YYYYMonday, 14 January 2025Full format

Period Constraints

// Maximum date range for queries
const MAX_DAYS_RANGE = 90 // 3 months maximum

// Validate date range
const validateDateRange = (startDate: string, endDate: string): boolean => {
const start = moment(startDate)
const end = moment(endDate)
const daysDiff = end.diff(start, 'days')

if (daysDiff > MAX_DAYS_RANGE) {
throw new Error(`Date range cannot exceed ${MAX_DAYS_RANGE} days`)
}

return true
}

Entry Details

Viewing Entry Details

Click on any entry to view full details:

// Entry detail structure
interface TimeEntryDetail {
id: string
date: string
clockIn: string
clockOut: string
breakDuration: number
totalHours: number
status: EntryStatus
notes: string | null

// Related data
shift?: {
id: string
scheduledStart: string
scheduledEnd: string
room: string
shiftType: string
}

// Audit information
createdAt: string
updatedAt: string
submittedAt?: string
approvedAt?: string
approvedBy?: string
}

Variance Display

Compare actual vs scheduled hours:

MetricMeaningDisplay
ScheduledExpected hours8.50 hrs
ActualRecorded hours8.00 hrs
VarianceDifference-0.50 hrs
// Variance calculation
const calculateVariance = (entry: TimeEntry, shift: Shift): number => {
const scheduledHours = moment(shift.end_time).diff(moment(shift.start_time), 'hours', true)
const actualHours = entry.total_hours || 0

return Math.round((actualHours - scheduledHours) * 100) / 100
}

Totals and Statistics

Summary Panel

The timesheet displays summary statistics:

// Summary statistics
interface TimesheetSummary {
period: {
start: string
end: string
label: string // e.g., "Week 3, January 2025"
}
hours: {
total: number // All hours
approved: number // Approved only
pending: number // Submitted, awaiting approval
draft: number // Not yet submitted
}
entries: {
total: number
complete: number // With clock out
incomplete: number // Missing clock out
}
overtime: {
total: number
threshold: number // e.g., 40 hours
}
}

Hours Breakdown Display

CategoryHoursStatus
Total Hours42.50-
Regular Hours40.00-
Overtime2.50Above threshold
Approved32.00
Pending8.00
Draft2.50📝

Identifying Issues

Common Issues to Check

Before submitting, review for these issues:

IssueIndicatorAction
Missing Clock OutNo end timeComplete the entry
Long Shift> 12 hoursVerify accuracy
No BreakBreak = 0 for 6+ hr shiftAdd break time
OverlapEntries overlapCorrect times
Rejected EntryRed statusReview and fix

Issue Detection

// Check entries for issues
const detectIssues = (entries: TimeEntry[]): Issue[] => {
const issues: Issue[] = []

entries.forEach(entry => {
// Missing clock out
if (!entry.clock_out_time) {
issues.push({
entryId: entry.id,
type: 'incomplete',
message: 'Missing clock out time'
})
}

// Long shift
if (entry.total_hours && entry.total_hours > 12) {
issues.push({
entryId: entry.id,
type: 'warning',
message: 'Shift exceeds 12 hours - please verify'
})
}

// No break for long shift
if (entry.total_hours && entry.total_hours >= 6 && !entry.break_duration) {
issues.push({
entryId: entry.id,
type: 'warning',
message: 'No break recorded for 6+ hour shift'
})
}

// Rejected status
if (entry.status === 'rejected') {
issues.push({
entryId: entry.id,
type: 'error',
message: 'Entry was rejected - requires correction'
})
}
})

return issues
}

Export Options

Exporting Timesheet Data

Available export formats:

FormatUse CaseContents
PDFPrinting, recordsFormatted report
CSVSpreadsheet analysisRaw data
ExcelDetailed analysisFormatted workbook

Export API

// Export timesheet to PDF
const exportTimesheet = async (startDate: string, endDate: string, format: 'pdf' | 'csv' | 'xlsx') => {
const response = await fetch(
`/api/staff/timesheet/export?start_date=${startDate}&end_date=${endDate}&format=${format}`
)

const blob = await response.blob()

// Download file
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `timesheet-${startDate}-to-${endDate}.${format}`
a.click()
}

Timesheet Component

Display Structure

// Timesheet view component structure
const TimesheetView = () => {
const [dateRange, setDateRange] = useState<DateRange>(getCurrentWeek())
const [statusFilter, setStatusFilter] = useState<EntryStatus | 'all'>('all')
const [entries, setEntries] = useState<TimeEntry[]>([])
const [totals, setTotals] = useState<TimesheetTotals | null>(null)
const [loading, setLoading] = useState(true)

return (
<div className="space-y-6">
{/* Header with date selection */}
<TimesheetHeader
dateRange={dateRange}
onDateRangeChange={setDateRange}
/>

{/* Filters */}
<TimesheetFilters
statusFilter={statusFilter}
onStatusFilterChange={setStatusFilter}
/>

{/* Summary cards */}
<TimesheetSummary totals={totals} />

{/* Entries list */}
<TimesheetEntries entries={entries} />

{/* Actions */}
<TimesheetActions entries={entries} />
</div>
)
}

Mobile View

Responsive Timesheet

On mobile devices:

  • Entries displayed as cards
  • Swipe to navigate weeks
  • Tap to expand entry details
  • 44px+ touch targets

Mobile Entry Card

// Mobile entry card layout
<div className="card-glass p-4 space-y-2">
<div className="flex justify-between">
<span className="font-medium">{formatDate(entry.date)}</span>
<StatusBadge status={entry.status} />
</div>
<div className="text-sm text-secondary-text">
{entry.clock_in_time} - {entry.clock_out_time}
</div>
<div className="flex justify-between">
<span>Total: {entry.total_hours} hrs</span>
<span>Break: {entry.break_duration} min</span>
</div>
</div>

Best Practices

For Reviewing Timesheet

  1. Review Daily - Check entries each day
  2. Verify Totals - Ensure hours are accurate
  3. Check Breaks - Confirm break time recorded
  4. Review Before Submit - Check all entries before submission
  5. Address Rejections - Fix rejected entries promptly

Weekly Review Checklist

CheckAction
All days recordedAdd any missing entries
Clock out completeComplete incomplete entries
Breaks accurateAdjust break times
No overlapsFix any overlapping entries
Totals correctVerify weekly total

Troubleshooting

Common Issues

IssueCauseSolution
Entries not loadingNetwork issueRefresh page
Wrong date rangeIncorrect selectionAdjust date picker
Missing entriesNot recordedCreate manual entries
Totals incorrectCalculation pendingRefresh or recalculate

Error Messages

ErrorMeaningAction
"Invalid date format"Wrong date formatUse YYYY-MM-DD
"Date range too large"> 90 days selectedReduce date range
"No entries found"No data for periodCheck date selection
"Session expired"Login timeoutLog in again


Source Files:

  • src/app/api/staff/timesheet/route.ts - Timesheet query API
  • src/components/staff/TimesheetView.tsx - Timesheet component
  • src/lib/utils/timezone.ts - UK date formatting
  • src/types/timesheet.ts - Type definitions