Skip to main content

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:

  1. Clock In - Records your shift start time
  2. Clock Out - Records your shift end time
  3. Automatic Calculation - System calculates total hours worked
  4. Break Deduction - Breaks are automatically deducted from total

Time Entry Status

StatusDescription
DraftEntry created but not submitted
SubmittedAwaiting manager approval
ApprovedHours confirmed by manager
RejectedHours queried - requires attention

Clocking In

From the Dashboard

  1. Navigate to Staff Dashboard
  2. Locate the Time Clock widget
  3. Click the Clock In button
  4. Confirm the current time is correct
  5. 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

  1. Navigate to Staff Dashboard
  2. The Time Clock widget shows you're clocked in
  3. Click the Clock Out button
  4. Add any break time if applicable
  5. 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

ScenarioClock InClock OutTotal Hours
Day Shift09:0017:308.5 hours
Evening Shift18:0023:005 hours
Night Shift22:0006:008 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

RestrictionLimitReason
Historical90 daysPayroll cut-off compliance
Future7 daysPrevent accidental entries
Maximum Shift24 hoursSafety and accuracy
Break Duration0-480 minutesReasonable 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

BenefitDescription
Variance TrackingCompare actual vs scheduled hours
Automatic DetailsPre-fill shift type and room
Reporting AccuracyBetter attendance reports
Pay Rate MatchingApply correct shift rates

Clock Status Indicators

Dashboard Display

StatusIconColourMeaning
Not Clocked InClockGreyReady to start shift
Clocked InClock SpinningGreenCurrently working
On BreakCoffeeAmberBreak in progress
Clocked OutCheckBlueShift 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

RequirementDetails
NotesStrongly recommended to explain reason
Manager ReviewMay require additional approval
Time AccuracyMust be within allowed date range
No OverlapsCannot 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

ScenarioErrorSolution
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

  1. Clock In Promptly - Within 5 minutes of shift start
  2. Clock Out When Leaving - Don't forget at shift end
  3. Record Breaks Honestly - Include all break time taken
  4. Add Notes - Explain any unusual circumstances
  5. Review Daily - Check entries before submitting

Common Mistakes to Avoid

MistakeConsequencePrevention
Forgetting to clock outOpen entry, no hours calculatedSet reminder
Wrong break durationIncorrect total hoursDouble-check before saving
Duplicate clock inEntry blockedCheck status before clocking in
Late clock in recordingMay need manager approvalClock in immediately

Troubleshooting

Common Issues

IssueCauseSolution
"Already clocked in"Previous entry not closedClock out first, then clock in
"Invalid time"Clock out before clock inCheck times are correct
"Entry too old"Beyond 90-day limitContact manager for assistance
"Overlap detected"Existing entry for this timeEdit existing entry instead

Error Messages

ErrorMeaningAction
"Clock in time is required"Missing clock inProvide clock in time
"Cannot clock out without clocking in"No active entryCreate new entry with both times
"Break duration exceeds shift length"Break too longReduce break duration
"Session expired"Login timeoutLog 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

  1. Open Shyfts app or web browser
  2. Dashboard loads with Time Clock prominent
  3. Tap Clock In button
  4. Confirm time (GPS optional)
  5. Later, tap Clock Out
  6. Enter break time
  7. Confirm to save


Source Files:

  • src/app/api/staff/timesheet/entries/route.ts - Time entries API
  • src/app/api/staff/timesheet/route.ts - Timesheet queries
  • src/components/staff/TimeClock.tsx - Time clock component
  • src/lib/utils/timezone.ts - UK timezone utilities