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:
- Daily View - Entries for a specific day
- Weekly View - Summary by week (Monday-Sunday)
- Period View - Custom date range selection
- Status Filtering - Filter by entry status
Accessing Your Timesheet
- Navigate to Staff Portal → Timesheet from the sidebar
- The current week's entries load by default
- 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| start_date | string | Yes | Start of date range (YYYY-MM-DD) |
| end_date | string | Yes | End of date range (YYYY-MM-DD) |
| status | string | No | Filter by entry status |
| include_totals | boolean | No | Include 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
| Status | Icon | Colour | Description |
|---|---|---|---|
| Draft | Pencil | Grey | Entry created, not submitted |
| Submitted | Clock | Blue | Awaiting manager approval |
| Approved | Check | Green | Confirmed by manager |
| Rejected | X | Red | Requires 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
| Column | Description | Format |
|---|---|---|
| Date | Entry date | DD/MM/YYYY |
| Clock In | Start time | HH:MM |
| Clock Out | End time | HH:MM |
| Break | Break duration | Minutes |
| Total | Net working hours | X.XX hrs |
| Status | Entry status | Badge |
| Actions | Edit/View options | Buttons |
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):
| Day | Position |
|---|---|
| Monday | Week start |
| Tuesday | Day 2 |
| Wednesday | Day 3 |
| Thursday | Day 4 |
| Friday | Day 5 |
| Saturday | Day 6 |
| Sunday | Week 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:
| Format | Example | Use |
|---|---|---|
| DD/MM/YYYY | 14/01/2025 | Standard display |
| ddd DD MMM | Mon 14 Jan | Short format |
| dddd, D MMMM YYYY | Monday, 14 January 2025 | Full 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:
| Metric | Meaning | Display |
|---|---|---|
| Scheduled | Expected hours | 8.50 hrs |
| Actual | Recorded hours | 8.00 hrs |
| Variance | Difference | -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
| Category | Hours | Status |
|---|---|---|
| Total Hours | 42.50 | - |
| Regular Hours | 40.00 | - |
| Overtime | 2.50 | Above threshold |
| Approved | 32.00 | ✅ |
| Pending | 8.00 | ⏳ |
| Draft | 2.50 | 📝 |
Identifying Issues
Common Issues to Check
Before submitting, review for these issues:
| Issue | Indicator | Action |
|---|---|---|
| Missing Clock Out | No end time | Complete the entry |
| Long Shift | > 12 hours | Verify accuracy |
| No Break | Break = 0 for 6+ hr shift | Add break time |
| Overlap | Entries overlap | Correct times |
| Rejected Entry | Red status | Review 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:
| Format | Use Case | Contents |
|---|---|---|
| Printing, records | Formatted report | |
| CSV | Spreadsheet analysis | Raw data |
| Excel | Detailed analysis | Formatted 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
- Review Daily - Check entries each day
- Verify Totals - Ensure hours are accurate
- Check Breaks - Confirm break time recorded
- Review Before Submit - Check all entries before submission
- Address Rejections - Fix rejected entries promptly
Weekly Review Checklist
| Check | Action |
|---|---|
| All days recorded | Add any missing entries |
| Clock out complete | Complete incomplete entries |
| Breaks accurate | Adjust break times |
| No overlaps | Fix any overlapping entries |
| Totals correct | Verify weekly total |
Troubleshooting
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Entries not loading | Network issue | Refresh page |
| Wrong date range | Incorrect selection | Adjust date picker |
| Missing entries | Not recorded | Create manual entries |
| Totals incorrect | Calculation pending | Refresh or recalculate |
Error Messages
| Error | Meaning | Action |
|---|---|---|
| "Invalid date format" | Wrong date format | Use YYYY-MM-DD |
| "Date range too large" | > 90 days selected | Reduce date range |
| "No entries found" | No data for period | Check date selection |
| "Session expired" | Login timeout | Log in again |
Related Documentation
- Clock In/Out - Record time
- Breaks - Break tracking
- Submitting Timesheet - Submit for approval
- Dashboard - Staff dashboard
Source Files:
src/app/api/staff/timesheet/route.ts- Timesheet query APIsrc/components/staff/TimesheetView.tsx- Timesheet componentsrc/lib/utils/timezone.ts- UK date formattingsrc/types/timesheet.ts- Type definitions