Submitting Timesheet
This guide covers how to submit your timesheet entries for manager approval, including submission requirements, the approval process, and handling rejected entries.
Overview
What is Timesheet Submission?
Submitting your timesheet sends your recorded hours to your manager for approval:
- Review Entries - Check all entries are accurate
- Submit for Approval - Send to manager
- Await Review - Manager reviews and approves/rejects
- Payroll Processing - Approved hours processed for payroll
Submission Workflow
Draft → Review → Submit → Manager Review → Approved/Rejected
↘ (If rejected) → Edit → Resubmit
Pre-Submission Requirements
Before You Submit
Ensure all entries meet these requirements:
| Requirement | Check |
|---|---|
| Complete entries | All have clock in AND clock out |
| Breaks recorded | Break time entered for 6+ hour shifts |
| No overlaps | Entries don't overlap each other |
| Within date limits | Not older than 90 days |
| Accurate times | Hours reflect actual work |
Validation Checks
// Source: src/app/api/staff/timesheet/submit/route.ts:65-110
// Pre-submission validation
const validateForSubmission = (entries: TimeEntry[]): ValidationResult => {
const errors: string[] = []
const warnings: string[] = []
entries.forEach(entry => {
// Check clock out exists
if (!entry.clock_out_time) {
errors.push(`Entry ${entry.id}: Missing clock out time`)
}
// Check for unusually long shifts
if (entry.total_hours && entry.total_hours > 12) {
warnings.push(`Entry ${entry.id}: Shift exceeds 12 hours`)
}
// Check break for long shifts
if (entry.total_hours >= 6 && !entry.break_duration) {
warnings.push(`Entry ${entry.id}: No break recorded for 6+ hour shift`)
}
})
return {
valid: errors.length === 0,
errors,
warnings
}
}
Submission Process
Step-by-Step Submission
- Navigate to Timesheet view
- Select the period to submit (typically current week)
- Review all entries for accuracy
- Click Submit for Approval
- Review the submission summary
- Confirm submission
Submission API
Endpoint: POST /api/staff/timesheet/submit
// Source: src/app/api/staff/timesheet/submit/route.ts:25-50
const submitTimesheetSchema = z.object({
entry_ids: z.array(z.string().uuid()).min(1, 'At least one entry required'),
submission_date: z.string().datetime().optional(),
notes: z.string().max(500).optional()
})
Submit Request Example
// Submit timesheet entries
const submitTimesheet = async (entryIds: string[], notes?: string) => {
const response = await fetch('/api/staff/timesheet/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
entry_ids: entryIds,
notes: notes || 'Weekly timesheet submission'
})
})
const data = await response.json()
if (data.success) {
console.log(`Submitted ${data.data.submitted_count} entries`)
}
return data
}
Response Format
// Submission response
{
success: true,
data: {
submitted_count: 5,
total_hours: 40.0,
period: {
start_date: "2025-01-13",
end_date: "2025-01-17"
},
entries: [
{
id: "entry-uuid-1",
status: "submitted",
submitted_at: "2025-01-18T10:00:00Z"
},
// ... more entries
]
}
}
Secure Submission (RPC)
Database Function
Timesheet submission uses a secure RPC function for data integrity:
// Source: src/app/api/staff/timesheet/submit/route.ts:150-200
// Secure RPC call for submission
const { data, error } = await supabase.rpc('submit_timesheet', {
p_staff_id: staffId,
p_entry_ids: entryIds,
p_submission_notes: notes
})
RPC Function Benefits
| Benefit | Description |
|---|---|
| Atomicity | All entries submitted together or none |
| Validation | Server-side validation enforced |
| Audit Trail | Submission timestamp recorded |
| Security | RLS policies enforced |
Automatic Hours Staff
Auto-Generated Entries
For staff with automatic hours calculation:
// Source: src/app/api/staff/timesheet/submit/route.ts:220-280
// Auto-generate entries from scheduled shifts
const generateAutoEntries = async (staffId: string, dateRange: DateRange) => {
// Fetch confirmed shifts without time entries
const { data: shifts } = await supabase
.from('shifts')
.select('*')
.eq('staff_id', staffId)
.eq('status', 'confirmed')
.gte('start_time', dateRange.start)
.lte('end_time', dateRange.end)
// Create time entries from shifts
const entries = shifts.map(shift => ({
staff_id: staffId,
clock_in_time: shift.start_time,
clock_out_time: shift.end_time,
break_duration: calculateAutoBreak(shift),
shift_id: shift.id,
status: 'draft',
notes: 'Auto-generated from scheduled shift'
}))
return entries
}
Auto-Generation Process
- System identifies scheduled shifts
- Creates time entries matching shift times
- Applies automatic break rules
- Entries marked as draft for review
- Staff can adjust before submission
Submission Summary
Before Confirmation
The submission summary shows:
// Submission summary structure
interface SubmissionSummary {
entriesCount: number
totalHours: number
period: {
startDate: string
endDate: string
}
breakdown: {
regularHours: number
overtimeHours: number
breakMinutes: number
}
warnings: string[]
readyToSubmit: boolean
}
Summary Display
| Metric | Value | Notes |
|---|---|---|
| Entries | 5 | Number of time entries |
| Total Hours | 42.5 | Sum of all hours |
| Regular Hours | 40.0 | Up to threshold |
| Overtime | 2.5 | Above threshold |
| Period | 13/01 - 17/01 | Submission period |
Submission Status
Entry Status Changes
When you submit:
| Before | After |
|---|---|
| Draft | Submitted |
| Rejected | Submitted (resubmission) |
Status Indicators
| Status | Badge | Meaning |
|---|---|---|
| Draft | Grey | Not submitted |
| Submitted | Blue | Awaiting approval |
| Approved | Green | Confirmed |
| Rejected | Red | Needs correction |
Manager Approval Process
What Happens After Submission
- Notification - Manager receives notification
- Review - Manager reviews entries
- Decision - Approve or reject
- Notification - You receive result notification
Approval Timeline
| Stage | Typical Time |
|---|---|
| Submission | Immediate |
| Manager notification | Within minutes |
| Review | 1-3 business days |
| Decision | Usually same pay period |
Handling Rejections
Why Entries Get Rejected
| Reason | Description |
|---|---|
| Inaccurate times | Hours don't match records |
| Missing breaks | Required breaks not recorded |
| Overtime query | Unexpected overtime |
| Discrepancy | Mismatch with scheduled shift |
| Documentation needed | Additional notes required |
Rejection Notification
// Rejection response structure
{
entryId: "entry-uuid",
status: "rejected",
rejectedAt: "2025-01-19T14:00:00Z",
rejectedBy: "manager-uuid",
rejectionReason: "Please verify clock in time - building access shows 09:15"
}
Resubmission Process
- View rejected entries in Timesheet
- Read the rejection reason
- Click Edit on the rejected entry
- Make necessary corrections
- Add notes explaining the correction
- Submit again for approval
// Resubmit corrected entry
const resubmitEntry = async (entryId: string, corrections: Partial<TimeEntry>) => {
// Update the entry
await fetch(`/api/staff/timesheet/entries/${entryId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...corrections,
notes: 'Corrected as requested - verified times with access logs'
})
})
// Resubmit
await fetch('/api/staff/timesheet/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
entry_ids: [entryId],
notes: 'Resubmission after correction'
})
})
}
Bulk Submission
Submitting Multiple Entries
Submit all draft entries at once:
// Bulk submit all draft entries in period
const bulkSubmit = async (startDate: string, endDate: string) => {
// Fetch all draft entries
const response = await fetch(
`/api/staff/timesheet?start_date=${startDate}&end_date=${endDate}&status=draft`
)
const { data } = await response.json()
const draftEntryIds = data.entries.map((e: TimeEntry) => e.id)
if (draftEntryIds.length === 0) {
throw new Error('No draft entries to submit')
}
// Submit all
return fetch('/api/staff/timesheet/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
entry_ids: draftEntryIds,
notes: `Bulk submission for ${startDate} to ${endDate}`
})
})
}
Submission Limits
| Limit | Value | Reason |
|---|---|---|
| Max entries per submission | 100 | Performance |
| Max date range | 90 days | Policy compliance |
| Minimum entries | 1 | At least one required |
Submission Deadlines
Payroll Cut-Off
Most organisations have submission deadlines:
| Deadline Type | Example |
|---|---|
| Weekly | Friday 17:00 for week's hours |
| Bi-weekly | Alternate Fridays |
| Monthly | Last day of month |
Late Submission
Late submissions may:
- Require manager approval for late entry
- Delay payroll processing
- Need additional documentation
Recall/Cancel Submission
Recalling Submitted Entries
If you need to make changes after submission:
// Recall submitted entry (before approval)
const recallEntry = async (entryId: string) => {
const response = await fetch(`/api/staff/timesheet/entries/${entryId}/recall`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
reason: 'Need to correct break time'
})
})
return response.json()
}
Recall Restrictions
| Status | Can Recall? |
|---|---|
| Submitted | ✅ Yes |
| Approved | ❌ No - contact manager |
| Rejected | N/A - already returned |
| Draft | N/A - not submitted |
Notifications
Submission Notifications
After submitting, notifications go to:
| Recipient | Notification |
|---|---|
| You | Confirmation of submission |
| Manager | Alert to review timesheet |
| Payroll | Copy (if configured) |
Email Notification Example
// Submission confirmation email
{
to: "staff@company.co.uk",
subject: "Timesheet Submitted - Week of 13/01/2025",
body: `
Your timesheet has been submitted for approval.
Period: 13/01/2025 - 17/01/2025
Total Hours: 40.0
Entries: 5
You will be notified when your manager reviews your timesheet.
`
}
Best Practices
For Successful Submission
- Submit Weekly - Don't let entries accumulate
- Review First - Check all entries before submitting
- Add Notes - Explain any unusual circumstances
- Submit Early - Don't wait until deadline
- Respond Quickly - Address rejections promptly
Submission Checklist
| Check | Status |
|---|---|
| ☐ All entries have clock out | Required |
| ☐ Breaks recorded for long shifts | Required |
| ☐ No overlapping entries | Required |
| ☐ Hours match scheduled shifts | Recommended |
| ☐ Notes added where needed | Recommended |
| ☐ Total hours verified | Recommended |
Troubleshooting
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| "Cannot submit incomplete entries" | Missing clock out | Complete all entries |
| "No entries to submit" | All already submitted | Check status filter |
| "Entries overlap" | Time conflict | Adjust entry times |
| "Submission failed" | Server error | Retry submission |
Error Messages
| Error | Meaning | Action |
|---|---|---|
| "At least one entry required" | No entries selected | Select entries to submit |
| "Entry is already submitted" | Duplicate submission | Entry already pending |
| "Entry validation failed" | Missing required data | Complete entry details |
| "Submission deadline passed" | Past cut-off | Contact manager |
Related Documentation
- Clock In/Out - Record time
- Breaks - Break recording
- Viewing Timesheet - Review entries
- Dashboard - Staff dashboard
Source Files:
src/app/api/staff/timesheet/submit/route.ts- Submission APIsrc/app/api/staff/timesheet/entries/[entryId]/recall/route.ts- Recall APIsrc/components/staff/TimesheetSubmission.tsx- Submission UIsupabase/functions/submit_timesheet.sql- RPC function