The homeowner’s view of contractor time tracking. Monitor live progress, review and approve hours, track costs against budget, and generate reports.
PosterTimeOverview — dashboard with live timer + job breakdown
useQuery(posterTimeOverview), useQuery(getActiveTimers)
PosterJobTimeLog — grouped by date, per-worker entries
useQuery(posterGetJobTimeEntries, { jobId }), approve/dispute actions
PosterTimeEntryDetail — full entry with map + photos
useQuery(posterGetTimeEntry, { entryId }), useMutation(approveEntry)
PosterWeeklySummary — calendar week view with cost calc
useQuery(posterWeeklySummary, { weekStart }), bar chart + rate math
PosterApproveTime — batch + individual approval queue
useQuery(posterPendingEntries), useMutation(batchApproveEntries)
PosterDisputeEntry — dispute form with reason + evidence
useMutation(posterDisputeTimeEntry), file upload via useUploadFile
PosterCostTracking — budget vs actual + projection
useQuery(posterJobCostSummary, { jobId }), derived calculations
PosterTimeReports — filterable export with breakdowns
useQuery(posterTimeReport, { dateRange, groupBy }), PDF/CSV generation
timeEntries {
jobId: Id<jobs>
contractorId: Id<contractorUsers>
clockInAt: number (timestamp)
clockOutAt: number?
clockInLocation: {lat, lng, accuracy}
taskDescription: string?
photos: Id<_storage>[]?
durationMinutes: number?
breakMinutes: number?
gpsVerified: boolean
gpsDistanceMeters: number?
approvalStatus: pending | approved |
disputed | adjusted
approvedAt: number?
approvedBy: Id<posterUsers>?
}
timeDisputes {
entryId: Id<timeEntries>
posterId: Id<posterUsers>
reason: hours_incorrect |
not_present | wrong_job |
work_incomplete | other
description: string
suggestedHours: number?
evidence: Id<_storage>[]?
status: open | resolved | escalated
}
posterTimeOverview()
→ {activeTimers, weeklyHours,
jobBreakdown, pendingCount}
posterGetJobTimeEntries(
jobId, dateRange?, limit?)
→ [{worker, hours, task, gps...}]
posterGetTimeEntry(entryId)
→ {clockIn, clockOut, worker,
task, photos, location, status}
posterWeeklySummary(weekStart)
→ {dailyHours[], totalHours,
costCalc, vsEstimate}
posterPendingEntries(jobId?)
→ entries with approvalStatus
== “pending”
posterJobCostSummary(jobId)
→ {budget, actual, projection,
workerCosts[], remaining}
posterTimeReport(
dateRange, groupBy, jobId?)
→ {byWorker[], byTask[],
totals, exportUrl}
approveTimeEntry(entryId,
notes?)
→ sets approvalStatus = approved,
triggers payment release
batchApproveEntries(
entryIds[], notes?)
→ bulk approve, returns count
posterDisputeTimeEntry(
entryId, reason, description,
suggestedHours?, evidence?)
→ creates timeDispute,
notifies contractor
generateTimeReport(
jobId?, dateRange, format)
→ {reportUrl, expiresAt}
Approval Statuses:
pending → awaiting poster review
approved → poster confirmed hours
disputed → poster challenged entry
adjusted → hours corrected post-dispute
Dispute Statuses:
open → awaiting contractor response
resolved → both parties agreed
escalated → admin intervention needed
GPS Thresholds:
verified → within 50m of job site
warning → 50–100m from site
flagged → >100m from site
Automations:
onEntryComplete → push notification
to poster for approval
onApproval → triggers escrow
payment release calculation
onDispute → Slack alert to ops,
email + in-app to contractor
autoApprove → GPS-verified entries
auto-approve after 72h if no action