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:
- Create Manual Entries - Add entries for forgotten clock-ins
- Edit Existing Entries - Correct wrong clock times
- Delete Invalid Entries - Remove duplicate or erroneous records
- Add Notes - Document reasons for corrections
When to Use Corrections
| Scenario | Action |
|---|---|
| Staff forgot to clock in | Create manual entry |
| Wrong clock-out time recorded | Edit existing entry |
| Duplicate entry created | Delete duplicate |
| System error during clock-in | Create/edit entry |
| Break not recorded | Edit 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
| Field | Type | Required | Description |
|---|---|---|---|
| staffId | UUID | Yes | Staff member for entry |
| workDate | Date | Yes | Date of work |
| clockInTime | DateTime | Yes | Clock in timestamp |
| clockOutTime | DateTime | Yes | Clock out timestamp |
| breakCycles | Array | No | Break periods |
| notes | String | Yes | Reason 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
| Field | Description | Notes |
|---|---|---|
| clock_in_time | Start time | Stores original in original_clock_in_time |
| clock_out_time | End time | Stores original in original_clock_out_time |
| break_cycles | Break periods | Recalculates total hours |
| notes | Entry notes | Append correction reason |
| status | Entry status | Limited 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
| Column | Description |
|---|---|
| Date | Work date |
| Staff | Employee name |
| Original Times | Before correction |
| Corrected Times | After correction |
| Corrected By | Manager who made correction |
| Reason | Notes 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:
| Field | Purpose |
|---|---|
manual_entry_by_user_id | Who made the correction |
manual_entry_by_role | Their role (manager/admin) |
original_clock_in_time | Time before first edit |
original_clock_out_time | Time before first edit |
notes | Reason for correction |
updated_at | When 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 Status | Can Delete? | Notes |
|---|---|---|
| active | Yes | Still being tracked |
| completed | Yes | Not yet submitted |
| submitted | With caution | Entry in review |
| approved | No | Already processed |
| rejected | Yes | Needs correction anyway |
Correction Workflow
Step-by-Step Process
-
Identify Issue
- Staff reports forgotten clock-in
- Review shows incorrect times
- Duplicate entry found
-
Verify Details
- Check scheduled shift
- Confirm with supervisor
- Review location/IP data
-
Make Correction
- Create manual entry, OR
- Edit existing entry, OR
- Delete invalid entry
-
Document Reason
- Add clear notes explaining why
- Reference verification source
- Include date of verification
-
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
- Always Document - Include clear notes for every correction
- Verify First - Confirm details before making changes
- Preserve Original - Never modify without tracking original
- Notify Staff - Keep employees informed of changes
For Preventing Issues
- Clock Reminders - Set up notifications for staff
- Break Enforcement - Require break logging
- Review Regularly - Check for anomalies weekly
- Train Staff - Ensure proper clock procedures
For Compliance
- Maintain Records - Keep correction history for 6 years
- Authorisation - Only managers can make corrections
- Transparency - Staff can view their correction history
- Audit - Regular review of correction patterns
Troubleshooting
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Cannot create entry | Overlap exists | Check for existing entries |
| Invalid break times | Outside shift range | Adjust break to be within clock times |
| Entry not saving | Validation error | Check all required fields |
| Cannot delete | Approved status | Contact admin for approved entries |
Error Messages
| Error | Meaning | Action |
|---|---|---|
| "Overlapping entry exists" | Time conflict | Adjust times or edit existing |
| "Clock out must be after clock in" | Invalid times | Correct time order |
| "Only managers can create manual entries" | Permission denied | Use manager account |
| "Cannot delete approved entries" | Entry finalised | Contact administrator |
Related Documentation
- Time Tracking Overview - Complete time tracking guide
- Viewing Timesheets - Timesheet management
- Time Reports - Analytics and reporting
Source Files:
src/app/api/time-tracking/corrections/route.ts- Corrections APIsrc/app/api/time-tracking/entries/[id]/route.ts- Entry CRUD APIsrc/types/database.types.ts:3161-3248- Time entries schemasrc/lib/db/time-tracking.db.ts- Database operations