Clock In/Out
This guide covers how to clock in at the start of your shift and clock out when you finish, ensuring accurate time records for payroll and reporting.
Overview
What is Clock In/Out?
The clock in/out system records your working hours:
- Clock In - Records your shift start time
- Clock Out - Records your shift end time
- Automatic Calculation - System calculates total hours worked
- Break Deduction - Breaks are automatically deducted from total
Time Entry Status
| Status | Description |
|---|---|
| Draft | Entry created but not submitted |
| Submitted | Awaiting manager approval |
| Approved | Hours confirmed by manager |
| Rejected | Hours queried - requires attention |
Clocking In
From the Dashboard
- Navigate to Staff Dashboard
- Locate the Time Clock widget
- Click the Clock In button
- Confirm the current time is correct
- Your shift begins recording
Clock In Validation
The system validates your clock in:
// Source: src/app/api/staff/timesheet/entries/route.ts:45-65
const clockInSchema = z.object({
clock_in_time: z.string().datetime().optional(),
shift_id: z.string().uuid().optional(),
notes: z.string().max(500).optional()
})
Clock In API
Endpoint: POST /api/staff/timesheet/entries
// Create a new time entry (clock in)
const response = await fetch('/api/staff/timesheet/entries', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clock_in_time: new Date().toISOString(),
shift_id: 'shift-uuid-optional',
notes: 'Starting morning shift'
})
})
const data = await response.json()
// Returns: { id: 'entry-uuid', clock_in_time: '2025-01-14T09:00:00Z', status: 'draft' }
Time Entry Created
// Time entry structure on clock in
{
id: "entry-uuid",
staff_id: "your-staff-uuid",
company_id: "company-uuid",
clock_in_time: "2025-01-14T09:00:00Z",
clock_out_time: null,
break_duration: null,
total_hours: null,
status: "draft",
notes: "Starting morning shift"
}
Clocking Out
From the Dashboard
- Navigate to Staff Dashboard
- The Time Clock widget shows you're clocked in
- Click the Clock Out button
- Add any break time if applicable
- Confirm to save your time entry
Clock Out Validation
// Source: src/app/api/staff/timesheet/entries/route.ts:67-95
const clockOutSchema = z.object({
clock_out_time: z.string().datetime(),
break_duration: z.number().min(0).max(480).optional(),
notes: z.string().max(500).optional()
})
Clock Out API
Endpoint: PUT /api/staff/timesheet/entries/[entryId]
// Update time entry (clock out)
const response = await fetch(`/api/staff/timesheet/entries/${entryId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clock_out_time: new Date().toISOString(),
break_duration: 30, // 30 minutes break
notes: 'Completed shift, 30 min lunch break taken'
})
})
const data = await response.json()
Hours Calculation
// Source: src/app/api/staff/timesheet/entries/route.ts:120-145
// Total hours calculation (server-side)
const calculateTotalHours = (entry: TimeEntry): number => {
if (!entry.clock_in_time || !entry.clock_out_time) return 0
const clockIn = moment.tz(entry.clock_in_time, UK_TIMEZONE)
const clockOut = moment.tz(entry.clock_out_time, UK_TIMEZONE)
// Calculate raw duration in hours
const rawHours = clockOut.diff(clockIn, 'hours', true)
// Deduct break time (convert minutes to hours)
const breakHours = (entry.break_duration || 0) / 60
// Return total rounded to 2 decimal places
return Math.round((rawHours - breakHours) * 100) / 100
}
Night Shifts
Handling Overnight Shifts
For shifts that cross midnight:
// Source: src/app/api/staff/timesheet/entries/route.ts:147-175
// Night shift detection
const isNightShift = (clockIn: string, clockOut: string): boolean => {
const inTime = moment.tz(clockIn, UK_TIMEZONE)
const outTime = moment.tz(clockOut, UK_TIMEZONE)
// Check if clock out is on the next day
return outTime.isBefore(inTime) || !inTime.isSame(outTime, 'day')
}
// Adjust clock out for next day if needed
if (isNightShift(clock_in_time, clock_out_time)) {
clockOutMoment = clockOutMoment.add(1, 'day')
}
Night Shift Example
| Scenario | Clock In | Clock Out | Total Hours |
|---|---|---|---|
| Day Shift | 09:00 | 17:30 | 8.5 hours |
| Evening Shift | 18:00 | 23:00 | 5 hours |
| Night Shift | 22:00 | 06:00 | 8 hours |
Time Restrictions
Historical Entries
// Source: src/app/api/staff/timesheet/entries/route.ts:48-52
// Maximum 90 days in the past
const MAX_HISTORICAL_DAYS = 90
// Validation check
const minDate = moment.tz(UK_TIMEZONE).subtract(MAX_HISTORICAL_DAYS, 'days')
if (clockInMoment.isBefore(minDate)) {
throw new Error('Cannot create entries more than 90 days in the past')
}
Future Entries
// Maximum 7 days in the future
const MAX_FUTURE_DAYS = 7
// Validation check
const maxDate = moment.tz(UK_TIMEZONE).add(MAX_FUTURE_DAYS, 'days')
if (clockInMoment.isAfter(maxDate)) {
throw new Error('Cannot create entries more than 7 days in the future')
}
Time Limits Summary
| Restriction | Limit | Reason |
|---|---|---|
| Historical | 90 days | Payroll cut-off compliance |
| Future | 7 days | Prevent accidental entries |
| Maximum Shift | 24 hours | Safety and accuracy |
| Break Duration | 0-480 minutes | Reasonable limits |
Linking to Scheduled Shifts
Automatic Association
When you clock in during a scheduled shift:
// Source: src/app/api/staff/timesheet/entries/route.ts:180-210
// Find matching scheduled shift
const findMatchingShift = async (staffId: string, clockInTime: string) => {
const clockIn = moment.tz(clockInTime, UK_TIMEZONE)
const { data: shifts } = await supabase
.from('shifts')
.select('*')
.eq('staff_id', staffId)
.eq('status', 'confirmed')
.lte('start_time', clockIn.toISOString())
.gte('end_time', clockIn.toISOString())
return shifts?.[0] || null
}
Benefits of Shift Linking
| Benefit | Description |
|---|---|
| Variance Tracking | Compare actual vs scheduled hours |
| Automatic Details | Pre-fill shift type and room |
| Reporting Accuracy | Better attendance reports |
| Pay Rate Matching | Apply correct shift rates |
Clock Status Indicators
Dashboard Display
| Status | Icon | Colour | Meaning |
|---|---|---|---|
| Not Clocked In | Clock | Grey | Ready to start shift |
| Clocked In | Clock Spinning | Green | Currently working |
| On Break | Coffee | Amber | Break in progress |
| Clocked Out | Check | Blue | Shift completed |
Current Status Check
// Check current clock status
const getCurrentStatus = async (staffId: string) => {
const { data: entry } = await supabase
.from('time_entries')
.select('*')
.eq('staff_id', staffId)
.is('clock_out_time', null)
.order('clock_in_time', { ascending: false })
.limit(1)
.single()
if (entry) {
return {
status: 'clocked_in',
since: entry.clock_in_time,
entryId: entry.id
}
}
return { status: 'not_clocked_in' }
}
Manual Time Entry
When to Use Manual Entry
- Forgot to clock in/out
- Technical issues prevented recording
- Working from a different location
- Retrospective time recording
Creating Manual Entry
// Manual time entry with both times
const response = await fetch('/api/staff/timesheet/entries', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clock_in_time: '2025-01-14T09:00:00Z',
clock_out_time: '2025-01-14T17:30:00Z',
break_duration: 30,
notes: 'Manual entry - forgot to clock in'
})
})
Manual Entry Requirements
| Requirement | Details |
|---|---|
| Notes | Strongly recommended to explain reason |
| Manager Review | May require additional approval |
| Time Accuracy | Must be within allowed date range |
| No Overlaps | Cannot overlap existing entries |
Overlap Detection
Preventing Duplicate Entries
// Source: src/app/api/staff/timesheet/entries/route.ts:215-245
// Check for overlapping entries
const checkOverlap = async (
staffId: string,
clockIn: string,
clockOut: string,
excludeEntryId?: string
) => {
let query = supabase
.from('time_entries')
.select('id, clock_in_time, clock_out_time')
.eq('staff_id', staffId)
.or(`clock_in_time.lte.${clockOut},clock_out_time.gte.${clockIn}`)
if (excludeEntryId) {
query = query.neq('id', excludeEntryId)
}
const { data: overlapping } = await query
if (overlapping && overlapping.length > 0) {
throw new Error('Time entry overlaps with existing entry')
}
}
Overlap Error Handling
| Scenario | Error | Solution |
|---|---|---|
| Exact Overlap | "Entry already exists for this time" | Edit existing entry |
| Partial Overlap | "Time entry overlaps with existing entry" | Adjust times |
| Already Clocked In | "You are already clocked in" | Clock out first |
Best Practices
For Accurate Time Recording
- Clock In Promptly - Within 5 minutes of shift start
- Clock Out When Leaving - Don't forget at shift end
- Record Breaks Honestly - Include all break time taken
- Add Notes - Explain any unusual circumstances
- Review Daily - Check entries before submitting
Common Mistakes to Avoid
| Mistake | Consequence | Prevention |
|---|---|---|
| Forgetting to clock out | Open entry, no hours calculated | Set reminder |
| Wrong break duration | Incorrect total hours | Double-check before saving |
| Duplicate clock in | Entry blocked | Check status before clocking in |
| Late clock in recording | May need manager approval | Clock in immediately |
Troubleshooting
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| "Already clocked in" | Previous entry not closed | Clock out first, then clock in |
| "Invalid time" | Clock out before clock in | Check times are correct |
| "Entry too old" | Beyond 90-day limit | Contact manager for assistance |
| "Overlap detected" | Existing entry for this time | Edit existing entry instead |
Error Messages
| Error | Meaning | Action |
|---|---|---|
| "Clock in time is required" | Missing clock in | Provide clock in time |
| "Cannot clock out without clocking in" | No active entry | Create new entry with both times |
| "Break duration exceeds shift length" | Break too long | Reduce break duration |
| "Session expired" | Login timeout | Log in again |
Mobile Clock In/Out
Using Mobile Device
On mobile devices:
- Large 44px+ touch-friendly buttons
- GPS confirmation option (if enabled)
- Quick swipe to clock in/out
- Push notifications for reminders
Mobile Workflow
- Open Shyfts app or web browser
- Dashboard loads with Time Clock prominent
- Tap Clock In button
- Confirm time (GPS optional)
- Later, tap Clock Out
- Enter break time
- Confirm to save
Related Documentation
- Break Tracking - Recording breaks
- Viewing Timesheet - Review your hours
- Submitting Timesheet - Submit for approval
- Dashboard - Staff dashboard overview
Source Files:
src/app/api/staff/timesheet/entries/route.ts- Time entries APIsrc/app/api/staff/timesheet/route.ts- Timesheet queriessrc/components/staff/TimeClock.tsx- Time clock componentsrc/lib/utils/timezone.ts- UK timezone utilities