Skip to main content

Time Tracking Overview

This guide covers the complete time tracking system in Shyfts, including clock-in/out functionality, break management, automatic hours mode, and timesheet workflows.


System Overview

What is Time Tracking?

Shyfts's time tracking system records staff working hours through:

  1. Clock In/Out - Staff record start and end of shifts
  2. Break Management - Multiple break cycles tracked per shift
  3. Automatic Hours - Hours calculated from scheduled availability
  4. Timesheet Submissions - Weekly/fortnightly review and approval

Time Entry Database Structure

// Source: src/types/database.types.ts:3161-3248
time_entries: {
Row: {
id: string
staff_id: string
company_id: string
shift_id: string | null
clock_in_time: string // Required - when staff clocked in
clock_out_time: string | null // Nullable until shift ends
break_cycles: Json | null // Array of break start/end times
break_duration: unknown // Calculated total break time
break_start_time: string | null // Legacy single break field
break_end_time: string | null // Legacy single break field
total_hours: number // Calculated working hours
status: string // active, completed, submitted, approved
entry_type: string | null // clock, manual, automatic
location_data: Json | null // Geolocation coordinates
ip_address: string | null // Device IP address
notes: string | null // Staff/manager notes
manual_entry_by_user_id: string | null // For manual corrections
manual_entry_by_role: string | null // Role of person who corrected
submission_id: string | null // FK to timesheet_submissions
approved_at: string | null // When entry was approved
approved_by: string | null // Who approved the entry
work_date: string | null // The date of work
original_clock_in_time: string | null // Before correction
original_clock_out_time: string | null // Before correction
}
}

Time Tracking Modes

1. Clock-Based Tracking

Staff manually clock in and out of shifts:

ActionDescription
Clock InRecords start time with optional geolocation
Start BreakPauses work timer, starts break cycle
End BreakResumes work timer, records break duration
Clock OutRecords end time, calculates total hours
// Source: src/components/time/TimeClock.tsx:82-95
const handleClockIn = async () => {
setLoading(true)
try {
// Get geolocation if available
let locationData = null
if ('geolocation' in navigator) {
try {
const position = await new Promise<GeolocationPosition>((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 5000
})
})
locationData = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy
}
} catch (geoError) {
console.warn('Geolocation not available:', geoError)
}
}

const response = await fetch('/api/time-tracking/clock-in', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ locationData })
})
// ...
}
}

2. Automatic Hours Mode

For staff with requires_clock_in = false, hours are calculated from scheduled availability:

SettingBehaviour
requires_clock_in = trueMust use clock in/out (default)
requires_clock_in = falseHours from scheduled shifts
// Source: src/app/api/staff/timesheet/submit/route.ts:185-220
// Generate automatic entries for staff who don't clock in
if (!staffData.requires_clock_in) {
// Get availability for date range
const { data: availability } = await supabase
.from('staff_availability')
.select('*')
.eq('staff_id', staffId)
.gte('date', startDate)
.lte('date', endDate)

// Create time entries from availability
for (const slot of availability) {
const hours = calculateHoursFromAvailability(slot)
await supabase.from('time_entries').insert({
staff_id: staffId,
company_id: companyId,
clock_in_time: slot.start_time,
clock_out_time: slot.end_time,
total_hours: hours,
entry_type: 'automatic',
status: 'completed'
})
}
}

Clock In/Out Process

Clock In Flow

  1. Staff accesses Time Clock from dashboard or menu
  2. System checks for existing active entry
  3. If no active entry, clock in button is available
  4. On click, system captures:
    • Current timestamp
    • IP address
    • Geolocation (if permitted)
  5. New time entry created with status active

API: Clock In

Endpoint: POST /api/time-tracking/clock-in

// Source: src/app/api/time-tracking/clock-in/route.ts:25-55
export async function POST(request: NextRequest) {
const supabase = await createClient()

// Get authenticated user
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

// Check for existing active entry
const { data: existingEntry } = await supabase
.from('time_entries')
.select('id')
.eq('staff_id', staffId)
.is('clock_out_time', null)
.single()

if (existingEntry) {
return NextResponse.json(
{ error: 'Already clocked in' },
{ status: 400 }
)
}

// Create new entry
const { data: entry, error } = await supabase
.from('time_entries')
.insert({
staff_id: staffId,
company_id: companyId,
clock_in_time: new Date().toISOString(),
status: 'active',
entry_type: 'clock',
location_data: body.locationData,
ip_address: request.headers.get('x-forwarded-for')
})
.select()
.single()

return NextResponse.json(entry)
}

Clock Out Flow

  1. Staff clicks clock out on active entry
  2. System calculates total hours (excluding breaks)
  3. Entry status changes to completed
  4. Entry available for timesheet submission

Endpoint: POST /api/time-tracking/clock-out


Break Management

Multiple Break Cycles

Shyfts supports multiple breaks per shift, stored as JSON array:

// Source: src/types/database.types.ts:3167
break_cycles: Json | null // Array of { start: timestamp, end: timestamp }

// Example break_cycles value:
[
{ "start": "2025-01-14T10:00:00Z", "end": "2025-01-14T10:15:00Z" },
{ "start": "2025-01-14T12:30:00Z", "end": "2025-01-14T13:00:00Z" }
]

Break Operations

OperationEndpointDescription
Start BreakPOST /api/time-tracking/break/startAdds new cycle with start time
End BreakPOST /api/time-tracking/break/endUpdates current cycle with end time
// Source: src/components/time/TimeClock.tsx:145-175
const handleBreakStart = async () => {
setLoading(true)
try {
const response = await fetch('/api/time-tracking/break/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ entryId: currentEntry.id })
})

if (response.ok) {
setOnBreak(true)
setBreakStartTime(new Date())
setBreakCount(prev => prev + 1)
}
} finally {
setLoading(false)
}
}

const handleBreakEnd = async () => {
setLoading(true)
try {
const response = await fetch('/api/time-tracking/break/end', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ entryId: currentEntry.id })
})

if (response.ok) {
setOnBreak(false)
// Calculate and add to total break duration
const breakDuration = Date.now() - breakStartTime.getTime()
setTotalBreakDuration(prev => prev + breakDuration)
}
} finally {
setLoading(false)
}
}

Break Duration Calculation

Total break time is calculated by summing all break cycles:

// Calculate total break duration from cycles
const calculateBreakDuration = (breakCycles: BreakCycle[]): number => {
return breakCycles.reduce((total, cycle) => {
if (cycle.start && cycle.end) {
const start = new Date(cycle.start).getTime()
const end = new Date(cycle.end).getTime()
return total + (end - start)
}
return total
}, 0)
}

Time Clock Interface

Full Time Clock Component

The main time clock component provides:

  • Current clock status display
  • Session timer (time since clock in)
  • Break timer (when on break)
  • Break count tracking
  • Clock in/out buttons
  • Break start/end buttons
// Source: src/components/time/TimeClock.tsx:250-320
return (
<div className="time-clock-container">
{/* Status Display */}
<div className="status-display">
<span className={`status-badge ${clockedIn ? 'active' : 'inactive'}`}>
{clockedIn ? (onBreak ? 'On Break' : 'Clocked In') : 'Not Clocked In'}
</span>
</div>

{/* Session Timer */}
{clockedIn && (
<div className="session-timer">
<h3>Session Time</h3>
<div className="time-display">
{formatDuration(sessionDuration)}
</div>
{onBreak && (
<div className="break-timer">
<span>Break Time: {formatDuration(currentBreakDuration)}</span>
</div>
)}
</div>
)}

{/* Action Buttons */}
<div className="action-buttons">
{!clockedIn ? (
<button onClick={handleClockIn} disabled={loading}>
Clock In
</button>
) : (
<>
{!onBreak ? (
<>
<button onClick={handleBreakStart} disabled={loading}>
Start Break
</button>
<button onClick={handleClockOut} disabled={loading}>
Clock Out
</button>
</>
) : (
<button onClick={handleBreakEnd} disabled={loading}>
End Break
</button>
)}
</>
)}
</div>

{/* Break Count */}
{clockedIn && breakCount > 0 && (
<div className="break-summary">
Breaks taken: {breakCount} | Total break time: {formatDuration(totalBreakDuration)}
</div>
)}
</div>
)

Dashboard Widget

A compact widget for quick access:

// Source: src/components/time/TimeClockWidget.tsx:85-130
return (
<div className="time-clock-widget">
<div className="widget-header">
<Clock className="icon" />
<span>Time Clock</span>
</div>

<div className="widget-status">
{clockedIn ? (
<>
<span className="status active">Clocked In</span>
<span className="duration">{formatDuration(sessionDuration)}</span>
</>
) : (
<span className="status inactive">Not Clocked In</span>
)}
</div>

<Link href="/staff/time-clock" className="widget-link">
Open Time Clock
</Link>
</div>
)

Time Entry Status

Status Workflow

┌─────────┐      ┌───────────┐      ┌───────────┐      ┌──────────┐
│ Active │ ───► │ Completed │ ───► │ Submitted │ ───► │ Approved │
└─────────┘ └───────────┘ └───────────┘ └──────────┘
│ │
│ ▼
│ ┌──────────┐
└────────────────────────────│ Rejected │
└──────────┘

Status Definitions

StatusDescription
activeStaff currently clocked in, shift in progress
completedShift finished, awaiting submission
submittedIncluded in timesheet submission for review
approvedManager approved the time entry
rejectedManager rejected, needs correction

Location Tracking

Geolocation Data

When enabled, clock-in captures location:

// Source: src/types/database.types.ts:3177
location_data: Json | null

// Example location_data value:
{
"latitude": 51.5074,
"longitude": -0.1278,
"accuracy": 10.5
}

IP Address Recording

// Source: src/app/api/time-tracking/clock-in/route.ts:45
ip_address: request.headers.get('x-forwarded-for')

Total Hours Calculation

Working Hours Formula

Total Hours = (Clock Out Time - Clock In Time) - Total Break Duration

Example Calculation

EventTimeCumulative
Clock In08:00-
Break Start10:002h worked
Break End10:1515m break
Break Start12:304h 15m worked
Break End13:0045m break
Clock Out17:008h total

Calculation: 9 hours (08:00-17:00) - 45 minutes breaks = 8.25 hours worked


Database Functions

Core Operations

// Source: src/lib/db/time-tracking.db.ts:25-80
// Get current active clock-in entry
export async function getCurrentClockIn(staffId: string) {
const supabase = await createClient()

const { data, error } = await supabase
.from('time_entries')
.select('*')
.eq('staff_id', staffId)
.is('clock_out_time', null)
.single()

return { data: data ? TimeTrackingAdapter.toComponent(data) : null, error }
}

// Get time entries list with filtering
export async function getTimeEntryList(params: {
staffId?: string
companyId?: string
startDate?: string
endDate?: string
status?: string
}) {
const supabase = await createClient()

let query = supabase
.from('time_entries')
.select(`
*,
staff:staff_id (first_name, last_name),
shift:shift_id (title, start_time, end_time)
`)

if (params.staffId) query = query.eq('staff_id', params.staffId)
if (params.companyId) query = query.eq('company_id', params.companyId)
if (params.startDate) query = query.gte('clock_in_time', params.startDate)
if (params.endDate) query = query.lte('clock_in_time', params.endDate)
if (params.status) query = query.eq('status', params.status)

const { data, error } = await query.order('clock_in_time', { ascending: false })

return {
data: data ? data.map(TimeTrackingAdapter.toComponent) : [],
error
}
}

Best Practices

For Staff

  1. Clock in promptly - Record actual start time
  2. Take scheduled breaks - Use break functionality correctly
  3. Add notes if needed - Explain late arrivals/early departures
  4. Submit timesheets on time - Follow company submission schedule

For Managers

  1. Review timesheets regularly - Don't let submissions pile up
  2. Use corrections sparingly - Document reasons for manual entries
  3. Set appropriate mode - Enable automatic hours where applicable
  4. Monitor location data - Verify clock-ins are from expected locations

For System Setup

  1. Configure clock-in requirements per staff member
  2. Set up geolocation if location verification needed
  3. Define break policies and monitor compliance
  4. Establish submission schedules (weekly/fortnightly)

Troubleshooting

Common Issues

IssueCauseSolution
Cannot clock inAlready have active entryClock out of existing entry first
Location not recordedGeolocation deniedGrant browser location permission
Wrong total hoursBreak not endedEnd all breaks before clocking out
Entry missingNot savedCheck network connection, retry

Error Messages

ErrorMeaningAction
"Already clocked in"Active entry existsClock out first
"No active entry"Trying to clock out without clock inClock in first
"Break already active"Trying to start second breakEnd current break first
"Unauthorized"Session expiredRe-authenticate


Source Files:

  • src/types/database.types.ts:3161-3248 - Time entries schema
  • src/components/time/TimeClock.tsx - Main time clock component
  • src/components/time/TimeClockWidget.tsx - Dashboard widget
  • src/lib/db/time-tracking.db.ts - Database operations
  • src/app/api/time-tracking/clock-in/route.ts - Clock in API
  • src/app/api/time-tracking/clock-out/route.ts - Clock out API
  • src/app/api/time-tracking/break/start/route.ts - Break start API
  • src/app/api/time-tracking/break/end/route.ts - Break end API