Approvals
Multi-level approval chains, period locking after final approval, notifications at every step. Single-level fallback for free workspaces.
Approvals
When a user submits their weekly timesheet, it walks an approval chain: a sequence of approvers that each have to approve before the next sees it. Free workspaces get a single level (workspace admins / managers approve everything). Time Pro unlocks routes per user / team / project / workspace, with stacked levels.
After the final level approves, all entries in the period are locked — no edits, no deletes — so finance teams can trust the numbers.
Screenshot needed — /time/approvals showing a queued timesheet with the chain preview ('After you, this goes to: Sarah → Finance').
The approval queue
/time/approvals is the inbox-style page where approvers see what needs their attention. Only timesheets where the current user is the current-level approver show up.
For each timesheet:
- Submitter name + period (
Mon Apr 28 – Sun May 4) - Total hours · Billable hours
- A chain preview: "After you, this goes to → next approver"
- Inline buttons: Approve · Reject with reason · View details
Bulk-approve with comment is supported when multiple are selected.
Multi-level chains (Pro)
Configurable at /time/settings/approvers. Define routes by scope + level:
| Scope type | Matches when | Example |
|---|---|---|
user | The submitter is this specific user | "All of Alice's timesheets go to Bob → Carol" |
team | The submitter belongs to this team | "Engineering team → Dave (lead) → Erin (manager)" |
project | The timesheet contains entries on this project | "Project Acme → Acme PM → Finance" |
workspace | Fallback for everything else | "Anyone else → Workspace admin" |
Routing picks the most-specific applicable scope first. Within a route, levels are sequential (1, 2, 3, …) — each must approve before the next sees it.
Fallback approvers
Each route step can have an optional fallback_user_id — used if the primary approver is OOO (per People module's time_off records) or has been deactivated. Falls back without manual intervention.
Editing routes
/time/settings/approvers shows every route grouped by scope. Edit inline; deletions cascade safely (active timesheets in flight finish on the chain that was active when they submitted).
Period locking (Pro)
When the final approval lands:
- All
time_entriesin the period getlocked_at = now(). - RLS policy denies UPDATE / DELETE on locked entries.
- The timesheet status flips from
submittedtoapproved.
Workspace admins can unlock a period via /time/timesheet/[id] → … → Unlock for editing. The unlock action is audit-logged and invalidates downstream invoices that pulled from the period (a notice surfaces).
Free workspaces don't get period locking — the equivalent guardrail is the standard status = 'approved' check at write time.
Single-level approval (free)
When time_approval_routes has no rows, the system falls back to the single-level model:
- Anyone with
time:approvepermission can approve any submitted timesheet. - That maps to workspace admins and the manager role by default.
- No chain preview, no period locking, no fallback approver logic.
Most small teams (≤10 people) live happily on this for the lifetime of the workspace.
Submitter side — what users see
When a user submits at the end of the week:
- Their timesheet flips from
drafttosubmitted. - The current-level approver gets an in-app notification + (if configured) email / Slack / Teams notification — all routed through the central
notificationService. - The user can see "Awaiting approval from Sarah (Team Lead)" on
/time/timesheet. - As each level approves, the chain advances. The user gets a notification when their timesheet reaches a new level.
- Final approval → notification "Your timesheet was approved (37.5h)".
Rejecting
Approvers can reject with a reason. The submitter gets a notification with the reason; the timesheet flips back to draft so they can edit and re-submit. The chain restarts from level 1 on resubmission.
Bulk-reject is supported but each rejected timesheet gets the same reason — for nuanced feedback, reject one at a time.
Auto-submit (Pro)
Don't want users to have to remember to submit? Set time_tracking_settings.auto_submit_dow to a day-of-week (e.g. Friday). A daily cron at 22:00 workspace-local checks: for any user whose draft timesheet for the current week has total_hours > 0, auto-submit it.
Users can opt out per-user via /settings/profile.
Notifications
| Event | Who gets notified | Channels |
|---|---|---|
| Timesheet submitted | Current-level approver | In-app + email + Slack/Teams (per channel prefs) |
| Approved (intermediate level) | Submitter | In-app |
| Approved (final) | Submitter | In-app + email |
| Rejected | Submitter | In-app + email |
| Reached new level | Submitter (informational) | In-app |
| OOO fallback used | Original approver + fallback approver | In-app |
All routing through @/lib/services/notification-service — never direct per-channel calls.
Next steps
- Time Pro — multi-level + period locking are Pro features
- Rates & Billing — approved entries are what feeds the invoice bridge
- Settings — auto-submit day-of-week, runaway-timer threshold