Skip to main content

Time Corrections

This guide covers manual time entry corrections, manager adjustments, and how to handle time tracking discrepancies in Shyfts.


Overview

What are Time Corrections?

Time corrections allow managers to:

  1. Create Manual Entries - Add entries for forgotten clock-ins
  2. Edit Existing Entries - Correct wrong clock times
  3. Delete Invalid Entries - Remove duplicate or erroneous records
  4. Add Notes - Document reasons for corrections

When to Use Corrections

ScenarioAction
Staff forgot to clock inCreate manual entry
Wrong clock-out time recordedEdit existing entry
Duplicate entry createdDelete duplicate
System error during clock-inCreate/edit entry
Break not recordedEdit to add break times

Correction Tracking Fields

Time entries track correction history:

// Source: src/types/database.types.ts:3180-3190
time_entries: {
Row: {
// ... other fields
manual_entry_by_user_id: string | null // Who made the correction
manual_entry_by_role: string | null // Role (manager/admin)
original_clock_in_time: string | null // Before correction
original_clock_out_time: string | null // Before correction
notes: string | null // Correction reason
}
}

Creating Manual Entries

When Staff Forgets to Clock In

Managers can create manual entries for staff who forgot to clock in:

Manual Entry API

Endpoint: POST /api/time-tracking/corrections

// Source: src/app/api/time-tracking/corrections/route.ts:25-120
export async function POST(request: NextRequest) {
const supabase = await createClient()
const body = await request.json()

const {
staffId,
clockInTime,
clockOutTime,
breakCycles,
notes,
workDate
} = body

// Validate manager permissions
const { data: { user } } = await supabase.auth.getUser()
const { data: staffData } = await supabase
.from('staff')
.select('role_id, company_id')
.eq('user_id', user.id)
.single()

if (!['company_manager', 'system_admin'].includes(staffData.role)) {
return NextResponse.json(
{ error: 'Only managers can create manual entries' },
{ status: 403 }
)
}

// Validate clock times
const clockIn = new Date(clockInTime)
const clockOut = new Date(clockOutTime)

if (clockOut <= clockIn) {
return NextResponse.json(
{ error: 'Clock out must be after clock in' },
{ status: 400 }
)
}

// Validate break cycles are within clock range
if (breakCycles && breakCycles.length > 0) {
for (const cycle of breakCycles) {
const breakStart = new Date(cycle.start)
const breakEnd = new Date(cycle.end)

if (breakStart < clockIn || breakEnd > clockOut) {
return NextResponse.json(
{ error: 'Break times must be within clock-in and clock-out range' },
{ status: 400 }
)
}
}
}

// Check for overlapping entries
const { data: existing } = await supabase
.from('time_entries')
.select('id')
.eq('staff_id', staffId)
.or(`and(clock_in_time.lte.${clockOutTime},clock_out_time.gte.${clockInTime})`)

if (existing && existing.length > 0) {
return NextResponse.json(
{ error: 'Overlapping time entry exists' },
{ status: 400 }
)
}

// Calculate total hours
const totalMs = clockOut.getTime() - clockIn.getTime()
const breakMs = calculateBreakDuration(breakCycles || [])
const totalHours = (totalMs - breakMs) / (1000 * 60 * 60)

// Create manual entry
const { data: entry, error } = await supabase
.from('time_entries')
.insert({
staff_id: staffId,
company_id: staffData.company_id,
clock_in_time: clockInTime,
clock_out_time: clockOutTime,
break_cycles: breakCycles,
break_duration: breakMs / (1000 * 60 * 60),
total_hours: totalHours,
work_date: workDate,
entry_type: 'manual',
status: 'completed',
manual_entry_by_user_id: user.id,
manual_entry_by_role: staffData.role,
notes: notes || 'Manual entry by manager'
})
.select()
.single()

return NextResponse.json(entry)
}

Manual Entry Form Fields

FieldTypeRequiredDescription
staffIdUUIDYesStaff member for entry
workDateDateYesDate of work
clockInTimeDateTimeYesClock in timestamp
clockOutTimeDateTimeYesClock out timestamp
breakCyclesArrayNoBreak periods
notesStringYesReason for manual entry

Example Request

{
"staffId": "staff-uuid",
"workDate": "2025-01-14",
"clockInTime": "2025-01-14T09:00:00Z",
"clockOutTime": "2025-01-14T17:30:00Z",
"breakCycles": [
{ "start": "2025-01-14T12:30:00Z", "end": "2025-01-14T13:00:00Z" }
],
"notes": "Staff forgot to clock in - verified with supervisor"
}

Editing Existing Entries

Edit Entry API

Endpoint: PATCH /api/time-tracking/entries/[id]

export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const supabase = await createClient()
const body = await request.json()
const entryId = params.id

// Get current entry
const { data: currentEntry } = await supabase
.from('time_entries')
.select('*')
.eq('id', entryId)
.single()

// Store original values before correction
const updates = {
...body,
original_clock_in_time: currentEntry.original_clock_in_time || currentEntry.clock_in_time,
original_clock_out_time: currentEntry.original_clock_out_time || currentEntry.clock_out_time,
manual_entry_by_user_id: userId,
manual_entry_by_role: userRole,
updated_at: new Date().toISOString()
}

// Recalculate total hours if times changed
if (body.clock_in_time || body.clock_out_time) {
const clockIn = new Date(body.clock_in_time || currentEntry.clock_in_time)
const clockOut = new Date(body.clock_out_time || currentEntry.clock_out_time)
const breakDuration = calculateBreakDuration(body.break_cycles || currentEntry.break_cycles || [])
updates.total_hours = ((clockOut - clockIn) - breakDuration) / (1000 * 60 * 60)
}

// Update entry
const { data: entry, error } = await supabase
.from('time_entries')
.update(updates)
.eq('id', entryId)
.select()
.single()

return NextResponse.json(entry)
}

Editable Fields

FieldDescriptionNotes
clock_in_timeStart timeStores original in original_clock_in_time
clock_out_timeEnd timeStores original in original_clock_out_time
break_cyclesBreak periodsRecalculates total hours
notesEntry notesAppend correction reason
statusEntry statusLimited to managers

Listing Corrections

View Corrected Entries

Endpoint: GET /api/time-tracking/corrections

// Source: src/app/api/time-tracking/corrections/route.ts:150-200
export async function GET(request: NextRequest) {
const supabase = await createClient()
const { searchParams } = new URL(request.url)

const companyId = searchParams.get('companyId')
const staffId = searchParams.get('staffId')
const startDate = searchParams.get('startDate')
const endDate = searchParams.get('endDate')

let query = supabase
.from('time_entries')
.select(`
*,
staff:staff_id (first_name, last_name),
corrected_by:manual_entry_by_user_id (first_name, last_name)
`)
.not('manual_entry_by_user_id', 'is', null) // Only manual/corrected entries

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

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

return NextResponse.json(data)
}

Corrections List View

ColumnDescription
DateWork date
StaffEmployee name
Original TimesBefore correction
Corrected TimesAfter correction
Corrected ByManager who made correction
ReasonNotes explaining correction

Validation Rules

Time Validation

// Validation for manual entries
const validateTimeEntry = (entry) => {
const errors = []

// Clock out must be after clock in
if (new Date(entry.clockOutTime) <= new Date(entry.clockInTime)) {
errors.push('Clock out must be after clock in')
}

// Maximum shift duration (16 hours)
const duration = (new Date(entry.clockOutTime) - new Date(entry.clockInTime)) / (1000 * 60 * 60)
if (duration > 16) {
errors.push('Shift duration cannot exceed 16 hours')
}

// Breaks must be within shift
if (entry.breakCycles) {
for (const cycle of entry.breakCycles) {
if (new Date(cycle.start) < new Date(entry.clockInTime)) {
errors.push('Break cannot start before clock in')
}
if (new Date(cycle.end) > new Date(entry.clockOutTime)) {
errors.push('Break cannot end after clock out')
}
}
}

// Work date must match clock-in date
const clockInDate = new Date(entry.clockInTime).toISOString().split('T')[0]
if (entry.workDate !== clockInDate) {
errors.push('Work date must match clock-in date')
}

return errors
}

Overlap Detection

// Source: src/app/api/time-tracking/corrections/route.ts:75-95
// Check for overlapping entries
const checkOverlap = async (staffId: string, clockIn: string, clockOut: string, excludeId?: string) => {
let query = supabase
.from('time_entries')
.select('id, clock_in_time, clock_out_time')
.eq('staff_id', staffId)
.or(`and(clock_in_time.lt.${clockOut},clock_out_time.gt.${clockIn})`)

if (excludeId) {
query = query.neq('id', excludeId)
}

const { data } = await query
return data && data.length > 0
}

Audit Trail

Tracking Changes

All corrections maintain an audit trail:

FieldPurpose
manual_entry_by_user_idWho made the correction
manual_entry_by_roleTheir role (manager/admin)
original_clock_in_timeTime before first edit
original_clock_out_timeTime before first edit
notesReason for correction
updated_atWhen last modified

Viewing History

// Get entry with correction history
const getEntryWithHistory = async (entryId: string) => {
const { data } = await supabase
.from('time_entries')
.select(`
*,
staff:staff_id (first_name, last_name),
corrected_by:manual_entry_by_user_id (first_name, last_name)
`)
.eq('id', entryId)
.single()

return {
...data,
wasModified: !!data.manual_entry_by_user_id,
originalTimes: data.original_clock_in_time ? {
clockIn: data.original_clock_in_time,
clockOut: data.original_clock_out_time
} : null
}
}

Deleting Entries

Delete Entry API

Endpoint: DELETE /api/time-tracking/entries/[id]

export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const supabase = await createClient()
const entryId = params.id

// Verify manager permissions
// ...

// Check if entry is part of approved submission
const { data: entry } = await supabase
.from('time_entries')
.select('submission_id, status')
.eq('id', entryId)
.single()

if (entry.status === 'approved') {
return NextResponse.json(
{ error: 'Cannot delete approved entries' },
{ status: 400 }
)
}

// Soft delete - mark as deleted
const { error } = await supabase
.from('time_entries')
.update({
status: 'deleted',
notes: `Deleted by ${userRole} on ${new Date().toISOString()}`
})
.eq('id', entryId)

return NextResponse.json({ success: true })
}

Deletion Rules

Entry StatusCan Delete?Notes
activeYesStill being tracked
completedYesNot yet submitted
submittedWith cautionEntry in review
approvedNoAlready processed
rejectedYesNeeds correction anyway

Correction Workflow

Step-by-Step Process

  1. Identify Issue

    • Staff reports forgotten clock-in
    • Review shows incorrect times
    • Duplicate entry found
  2. Verify Details

    • Check scheduled shift
    • Confirm with supervisor
    • Review location/IP data
  3. Make Correction

    • Create manual entry, OR
    • Edit existing entry, OR
    • Delete invalid entry
  4. Document Reason

    • Add clear notes explaining why
    • Reference verification source
    • Include date of verification
  5. Notify Staff (optional)

    • Inform staff of correction
    • Explain impact on hours

Correction Form UI

// Correction form component structure
const CorrectionForm = ({ staffId, entry }) => {
const [formData, setFormData] = useState({
staffId: entry?.staff_id || staffId,
workDate: entry?.work_date || '',
clockInTime: entry?.clock_in_time || '',
clockOutTime: entry?.clock_out_time || '',
breakCycles: entry?.break_cycles || [],
notes: ''
})

const handleSubmit = async () => {
// Validate
const errors = validateTimeEntry(formData)
if (errors.length > 0) {
setErrors(errors)
return
}

// Submit
if (entry) {
await updateEntry(entry.id, formData)
} else {
await createManualEntry(formData)
}
}

return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
</form>
)
}

Best Practices

For Making Corrections

  1. Always Document - Include clear notes for every correction
  2. Verify First - Confirm details before making changes
  3. Preserve Original - Never modify without tracking original
  4. Notify Staff - Keep employees informed of changes

For Preventing Issues

  1. Clock Reminders - Set up notifications for staff
  2. Break Enforcement - Require break logging
  3. Review Regularly - Check for anomalies weekly
  4. Train Staff - Ensure proper clock procedures

For Compliance

  1. Maintain Records - Keep correction history for 6 years
  2. Authorisation - Only managers can make corrections
  3. Transparency - Staff can view their correction history
  4. Audit - Regular review of correction patterns

Troubleshooting

Common Issues

IssueCauseSolution
Cannot create entryOverlap existsCheck for existing entries
Invalid break timesOutside shift rangeAdjust break to be within clock times
Entry not savingValidation errorCheck all required fields
Cannot deleteApproved statusContact admin for approved entries

Error Messages

ErrorMeaningAction
"Overlapping entry exists"Time conflictAdjust times or edit existing
"Clock out must be after clock in"Invalid timesCorrect time order
"Only managers can create manual entries"Permission deniedUse manager account
"Cannot delete approved entries"Entry finalisedContact administrator


Source Files:

  • src/app/api/time-tracking/corrections/route.ts - Corrections API
  • src/app/api/time-tracking/entries/[id]/route.ts - Entry CRUD API
  • src/types/database.types.ts:3161-3248 - Time entries schema
  • src/lib/db/time-tracking.db.ts - Database operations