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:
- Clock In/Out - Staff record start and end of shifts
- Break Management - Multiple break cycles tracked per shift
- Automatic Hours - Hours calculated from scheduled availability
- 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:
| Action | Description |
|---|---|
| Clock In | Records start time with optional geolocation |
| Start Break | Pauses work timer, starts break cycle |
| End Break | Resumes work timer, records break duration |
| Clock Out | Records 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:
| Setting | Behaviour |
|---|---|
requires_clock_in = true | Must use clock in/out (default) |
requires_clock_in = false | Hours 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
- Staff accesses Time Clock from dashboard or menu
- System checks for existing active entry
- If no active entry, clock in button is available
- On click, system captures:
- Current timestamp
- IP address
- Geolocation (if permitted)
- 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
- Staff clicks clock out on active entry
- System calculates total hours (excluding breaks)
- Entry status changes to
completed - 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
| Operation | Endpoint | Description |
|---|---|---|
| Start Break | POST /api/time-tracking/break/start | Adds new cycle with start time |
| End Break | POST /api/time-tracking/break/end | Updates 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
| Status | Description |
|---|---|
| active | Staff currently clocked in, shift in progress |
| completed | Shift finished, awaiting submission |
| submitted | Included in timesheet submission for review |
| approved | Manager approved the time entry |
| rejected | Manager 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
| Event | Time | Cumulative |
|---|---|---|
| Clock In | 08:00 | - |
| Break Start | 10:00 | 2h worked |
| Break End | 10:15 | 15m break |
| Break Start | 12:30 | 4h 15m worked |
| Break End | 13:00 | 45m break |
| Clock Out | 17:00 | 8h 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
- Clock in promptly - Record actual start time
- Take scheduled breaks - Use break functionality correctly
- Add notes if needed - Explain late arrivals/early departures
- Submit timesheets on time - Follow company submission schedule
For Managers
- Review timesheets regularly - Don't let submissions pile up
- Use corrections sparingly - Document reasons for manual entries
- Set appropriate mode - Enable automatic hours where applicable
- Monitor location data - Verify clock-ins are from expected locations
For System Setup
- Configure clock-in requirements per staff member
- Set up geolocation if location verification needed
- Define break policies and monitor compliance
- Establish submission schedules (weekly/fortnightly)
Troubleshooting
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Cannot clock in | Already have active entry | Clock out of existing entry first |
| Location not recorded | Geolocation denied | Grant browser location permission |
| Wrong total hours | Break not ended | End all breaks before clocking out |
| Entry missing | Not saved | Check network connection, retry |
Error Messages
| Error | Meaning | Action |
|---|---|---|
| "Already clocked in" | Active entry exists | Clock out first |
| "No active entry" | Trying to clock out without clock in | Clock in first |
| "Break already active" | Trying to start second break | End current break first |
| "Unauthorized" | Session expired | Re-authenticate |
Related Documentation
- Viewing Timesheets - Review staff timesheets
- Time Corrections - Manual entry corrections
- Time Reports - Time tracking analytics
Source Files:
src/types/database.types.ts:3161-3248- Time entries schemasrc/components/time/TimeClock.tsx- Main time clock componentsrc/components/time/TimeClockWidget.tsx- Dashboard widgetsrc/lib/db/time-tracking.db.ts- Database operationssrc/app/api/time-tracking/clock-in/route.ts- Clock in APIsrc/app/api/time-tracking/clock-out/route.ts- Clock out APIsrc/app/api/time-tracking/break/start/route.ts- Break start APIsrc/app/api/time-tracking/break/end/route.ts- Break end API