#Time tracking and approvals
Module id: MOD-TIME-TRACKING
Doc profile: capability
Contractors record live hours and breaks, submit reports for company review, and companies approve filled time for payment send. Backend workers ensure tracking rows exist and mark approval notices when the shift window is due — replacing Bubble’s recursive send_tracking_approve with a queue/cursor job (LOG-RECURSIVE). End job popup is owned by MOD-COMPANY-SHIFTS; this module hosts it for report Submit only.
#Purpose
Give contractors a signed-in place to start and stop shift timers, edit logged duration, and submit completed entries; give companies a time-tracking tab to filter and approve those entries; and run server-side ensure/approve-notice jobs so rows and approval_notice_sent stay consistent without client service-role writes (MON-CLIENT-WRITES-ALL). Observable outcomes are time_entries rows (from Bubble time_tracking1) under RLS, with create/stop/update/approve going through JWT RPCs or service-role edge workers. Live Start time may set company_requested true or false from the begin-shift “Company request” checkbox; backend ensure-row create always leaves that flag false.
#Scope boundaries
| In scope | Out of scope |
|---|---|
/contractor/time-tracking live timer + reports tabs |
Workspace Header / SideBar (MOD-SHELL) |
Company tab body on /company/shifts?tab=time_tracking |
Company dashboard host / other tabs (MOD-COMPANY-SHIFTS) |
time_entries schema, RLS, create/stop/update/approve RPCs |
Inventing reject UX (source has Approve → Approved only; not_approved deleted) |
| Queue redesign for create-list-tracking + send-tracking-approve | Line-by-line Bubble ScheduleAPIEvent self-recursion |
| Fifteen-minute delay job (list visibility + clear delay marker) | Attaching tracking-approve side effects to the delay job |
| Host EndJobPopup instance on report Submit | Forking EndJobPopup / Postmark “New time-tracking report” implement tree |
| Break enum 10–60 min step 5; tracking mode tabs | Stripe Connect / payouts (MOD-PAYMENTS / MON-MARKETPLACE) |
| Enqueue consumer for create-list-tracking | Owning company shift publish that enqueues create-list (MOD-COMPANY-SHIFTS) |
Module-map primary_journeys still say company “approve or reject” and that the fifteen-minute delay “resumes” tracking approve/create. Export chase supersedes both: company tab is Approve → Approved only, and the delay job only adjusts list visibility + clears its marker (J4 / J5). Refresh the map on the next map pass; do not reintroduce reject or delay→approve coupling from the stale map wording.
#Actors
| Actor | Behavior in this module |
|---|---|
| Contractor | Opens time-tracking; starts/stops timers (optional Company request on begin); edits times; browses reports; Submit opens shared End job popup; Check opens notes. |
| Company operator | Opens Time-tracking tab; filters Zero/Filled hours; approves one or many entries; edits contractors’ note (also marks sent-for-payment). |
| Session client | Anon-key reads under RLS; mutations via user JWT RPCs only — never service role in the browser. |
| Tracking workers | Service-role edge/cron: ensure time_entries for contractor+shift; mark approval notice when window due; re-queue without HTTP self-recursion. |
| Delay worker | Service-role job for fifteen-minute delay: add user to contractor-list visibility set and clear delay marker. |
#Capability composition
Contractor time-tracking is one cabinet route with two modes (live vs reports). Company approval lives as a tab body inside the company shifts host. Backend ensure/approve-notice and fifteen-minute delay are edge/cron surfaces, not SPA pages. Shared End job popup is imported from company-shifts.
Bubble evidence for pages, APIs, and tab body is under Traceability / IR evidence.
#Primary journeys
Journeys are reconstructed from contractor page workflows, company dashboard time-tracking workflows, and API events create_list_tracking / send_tracking_approve / 15_minuts_delay. Where End job popup internals or company shift publish enqueue belong to another module, steps are marked out of scope.
#J1 — Contractor live-tracks a shift
- A signed-in contractor opens
/contractor/time-tracking(default ortab=time_tracking). - Signed-out visitors are redirected to
/; the page stamps the contractor time-tracking root under the cabinet workspace. - Mode shows “Tracking your time” (vs “Time tracking reports”); live mode shows “Start tracking your time”, today’s date, and “Begin shift”.
- Begin shift opens “New time entry for YYYY/MM/DD” with shift picker and Checkbox Company request; Cancel hides; Start time creates an in-progress
time_entriesrow via RPC (self contractor, chosen shift,started_atnow) withcompany_requestedtrue when Company request is checked and false when it is not — do not treat backend ensure-row’s always-false create as this path. - Stop ends the timer via RPC with logged hours/minutes/seconds; break (“If you had a break:”) applies only when elapsed ≥ 15 minutes — otherwise the less-than-15-minutes message blocks break selection.
- Editing start/end calls update RPC and recomputes logged fields; optional
?shift=focuses that shift or shows a clear empty/error state. - RPC failures show inline/page errors without inventing client-only persistence.
#J2 — Contractor reviews reports and submits
- Contractor selects “Time tracking reports” and sees the reports list (Date, Job title, Company, Location, Start T, End T, Break T, Logged T, Notes, Approve, and Upcoming payment when bound) or “No result found”.
- Approve column is read-only on this surface (waiting vs company-approved labels).
- Check opens the notes popup for the row’s contractor notes.
- Submit opens the shared End job popup host for the selected entry — implement reuses MOD-COMPANY-SHIFTS End job popup; does not fork it.
- After a successful shared confirm, the list refreshes; Cancel/hide returns without orphan overlays.
- Postmark “New time-tracking report” and End job popup write paths remain owned by the shared reusable (out of this module’s implement tree).
#J3 — Backend ensures tracking rows and approval notices
- An enqueue for contractor + shift runs the create-list-tracking equivalent: if no
time_entriesrow exists for that pair, insert one withcompany_requestedfalse andapproval_notice_sentfalse; if a row already exists, skip create (idempotent). This always-false create is only the ensure path — not the contractor live Start time branch in J1. - After create (or when the row exists), enqueue send-tracking-approve work and persist an opaque
schedule_send_approve_job_idwhen a follow-up is scheduled. - When the approval window is due — shift date plus hours required, plus one day, is less than now — the worker sets
approval_notice_senttrue. - When the window is not due, the worker re-queues with backoff/cursor and must not HTTP-schedule itself (LOG-RECURSIVE).
- Upstream company shift publish that enqueues this consumer stays MOD-COMPANY-SHIFTS; this module owns the consumer redesign only.
#J4 — Company reviews and approves time entries
- A company user opens
/company/shifts?tab=time_trackingand sees filters “Select period”, “Select engagement”, “Approval status” plus “Zero hours” / “Filled hours”. - The list shows Date, Job title, Worker, Location, Logged time, Approval, Contractors’ note — or “Nothing was found. Try again later!”.
- Row shows “Approve” when not company-approved and “Approved” when true; Approve opens confirm “Please, confirm your action” / “You are going to send this shift for payment.” with Save / Discard.
- Save calls
set_company_approvalsettingcompany_approvedandsent_for_paymenttrue; Discard writes nothing. - “Multipal approval” (source spelling) bulk-applies the same flags via one server round-trip.
- Contractors’ note popup Save persists company notes and sets
sent_for_paymenttrue (same paid flag as Approve Save); Close dismisses. - There is no separate reject control on this surface — only Approve → Approved.
#J5 — Fifteen-minute delay clears list visibility marker
- A company (or system) caller schedules the fifteen-minute delay job for a target user.
- When the job runs as service role, it adds the user to the show-contractor-list visibility set and clears the schedule-15-min-delay marker.
- A second run with the marker already clear is a successful no-op.
- The job does not run create-list-tracking or send-tracking-approve — those stay on J3.
#Vertical slices (decompose)
| Item | Product outcome |
|---|---|
| TT-SCHEMA | time_entries migration, enums, RLS, and create/stop/update/approve RPCs (infrastructure). |
| TT-LIVE-TRACK | Contractor live timer start/stop/edit with Company request checkbox and break rules on /contractor/time-tracking. |
| TT-REPORTS-SUBMIT | Reports list, notes check, Submit hosting End job popup. |
| TT-APPROVE-QUEUE | Queue/cursor redesign for ensure-row + approval-notice (LOG-RECURSIVE). |
| TT-COMPANY-REVIEW | Company time-tracking tab filters, approve, bulk approve, notes (notes Save also sets sent-for-payment). |
| TT-DELAY-JOB | Fifteen-minute delay worker: list visibility + clear delay marker. |
Decompose TT-LIVE-TRACK still echoes an AC that Start time always passes company_requested=false; implement must follow the checkbox branch documented in J1 / IR evidence, not that oversimplification.
#Key UI states
| State | When | User sees |
|---|---|---|
| Live mode | Tracking your time selected | Begin shift, today label, active Stop / break |
| Reports mode | Time tracking reports selected | Reports table or “No result found” |
| New entry popup | Begin shift | Shift picker, Checkbox Company request, Start time / Cancel |
| Company request checked | Checkbox has data on Start time | New row with company_requested true |
| Company request unchecked | Checkbox has no data on Start time | New row with company_requested false |
| Break blocked | Stop with elapsed < 15 minutes | Less-than-15-minutes break message |
| Company Zero / Filled hours | Mode chips on company tab | Filtered list or empty copy |
| Approve vs Approved | company_approved false / true |
Approve control or Approved label |
| Approve confirm open | Approve clicked | Payment-send confirm Save / Discard |
| End job popup host open | Reports Submit | Shared End job popup for selected entry |
#Style and fidelity
Cabinet workspace outlet for contractor route and company tab body. Transcribe visible copy and stamp data-bubble-id on migrated roots: contractor page, mode toggle, live/report regions, begin-shift popup (including Company request checkbox), reports RG, company time-tracking group and RG, approve and notes popups. Preserve source spelling “Multipal approval”. Shared tokens come from the app-level stylesheet that SHELL-LAYOUT (MOD-SHELL) delivers out of IR styles (ir/slices/styles____part_all.json — 49 styles, 8 color tokens, 1 font token); this cycle's module map has no separate design-system module, so do not defer a token to one. Do not restyle reports into a card grid — keep table/list hierarchy.
#Failure modes
| Case | Expected behavior |
|---|---|
| Signed-out on contractor time-tracking | Redirect to /; no timer UI. |
| Start / stop / update RPC failure | Inline or page error; no silent client-only row. |
| Break selected under 15 minutes | Block break; show less-than-15-minutes message. |
Invalid / empty ?shift= |
Clear empty or error state; do not invent shifts. |
| Reports load empty | “No result found”. |
| End job popup failure (shared) | Surface error to contractor; list unchanged until success. |
| Company list / filter failure | Plain-language error; no fake empty success. |
| Approve / bulk / notes RPC failure | Visible error; prior approval and sent-for-payment flags unchanged. |
| Ensure-row enqueue duplicate | Still one time_entries row (idempotent). |
| Approval window not due | Re-queue; never recursive HTTP self-schedule. |
| Delay job unauthorized invoke | Fail closed; no profile mutation. |
Anon mutate time_entries |
RLS / RPC deny. |
#Traceability
| Product concept | Target (route / component / table) | Bubble evidence |
|---|---|---|
| Contractor time-tracking page | /contractor/time-tracking, ContractorTimeTrackingPage |
bTJFi0 |
| Signed-out redirect | ChangePage → index | bTLsP |
| Live / reports mode | tracking option → tab state |
tracking, bTLcl, bTLcp, bTLcq |
| Begin shift / new entry | BeginShiftPopup |
bTJIn0, bTJIt0, bTLXf |
| Company request checkbox | begin-shift popup control | bTJJZ0 |
| Start time create (requested) | create_time_entry RPC, company_requested true |
bTLYK, bTLar |
| Start time create (not requested) | create_time_entry RPC, company_requested false |
bTLYK, bTLat |
| Stop + break | stop_time_entry RPC, BreakSelect |
bTJMp0, break, bTLZL |
| Edit start/end inputs | update_time_entry_times RPC |
bTMEa |
| Shift deep-link | ?shift= focus / empty |
bTLen |
| Reports list | ReportsPanel |
bTJLz0, bTJMd0 |
| Empty reports | “No result found” | bTJMv0 |
| Submit → End job host | EndJobPopupHost → shared EndJobPopup |
bTJNl0, bTJPv0, bTHkx |
| Notes check | NotesCheckPopup |
bTJOP0, bTJOc0 |
| Postmark new report (shared) | owned by End job popup | external_call:bTNMZ:bTNMs, external_ns:bTNMZ |
| Ensure tracking row API | edge create-list-tracking |
bTLsx, bTNLv1 |
| Schedule approve after create | enqueue send-tracking-approve | bTMCF, bTMCP |
| Approval notice + recursion | edge send-tracking-approve |
bTMBg, bTMCE, bTMCQ |
| Company time-tracking tab | CompanyTimeTrackingTab |
bTKSh0, bTKbH4 |
| Single approve | set_company_approval |
bTLPX1, bTMSH |
| Bulk approve | set_company_approval_bulk |
bTMTN, bTMTZ |
| Contractors’ note save | company notes + sent-for-payment | bTLhF, bTLhL |
| Fifteen-minute delay API | edge fifteen-min-delay |
bTKPV, bTKPa, bTKVs1 |
| Delay schedule caller | company dashboard ScheduleAPIEvent | bTKPf |
| Time entries entity | public.time_entries |
time_tracking1 |
| Approval display option | labels only; boolean is source of truth | approve_type, bTLhj, bTLho |
| Deleted not-approved value | omit | bTLhn (residual dead) |
Empty option set time_tracking |
omit | time_tracking (residual dead) |
| Deleted feedback field | omit column | feedback_text (residual dead) |
#IR evidence
#Module evidence roots
bTJFi0— contractor_time-tracking page (/pages/bTJIF0); live timer, reports, PageLoaded auth, Submit host for EndJobPopupbTLsx— API eventcreate_list_tracking(/api/bTLsy); NewThing when count of matchingtime_tracking1for contractor+shift is 0; then schedulesend_tracking_approveand store schedule idbTMBg— API eventsend_tracking_approve(/api/bTMBh); when window due setsend_booleantrue; else ScheduleAPIEvent self (bTMCQ) — target must become queue/cursor (LOG-RECURSIVE)bTKPV— API event15_minuts_delay(/api/bTKPZ); add user toshow_contractor_list_user, clearschedule_15_min_delay_text
Supporting segments: ir/slices/bTJIF0____root.json, ir/slices/edef_bTHky____root.json, bTHkx (deferred EndJobPopup), time_tracking1, approve_type, break, external_ns:bTNMZ.
#Path-key ↔ inventory id aliases
| Inventory id | Export path key |
|---|---|
bTJFi0 |
/pages/bTJIF0 |
bTLsx |
/api/bTLsy |
bTMBg |
/api/bTMBh |
bTKPV |
/api/bTKPZ |
bTHkx |
/element_definitions/bTHky |
bTKSh0 |
/pages/bTIbd/elements/bTHQp/elements/bTKSm0 |
#Start time / company_requested (chased)
Workflow bTLYK (ButtonClicked Start time on begin-shift popup) has two NewThing actions on custom.time_tracking1:
bTLar— when Checkbox Company requestbTJJZ0has data (get_data): setscompany_request_booleantrue (plus contractor = CurrentUser, shift from pickerbTJJR0,start_date= now).bTLat— whenbTJJZ0has no data (get_not_data): same contractor/shift/start fields withcompany_request_booleanfalse.
Do not conflate with create_list_tracking action bTNLv1, which always initializes company_request_boolean false on ensure-row create.
#Approval window (chased)
On send_tracking_approve action bTMCE, send_boolean is set true when:
track.shift.date_date + hours_required_number (plus_hours) + 1 day (plus_days) less_than Current Date/Time.
If that ChangeThing does not apply (PreviousStep empty), action bTMCQ schedules bTMBg again — rewrite as re-queue, not HTTP self-schedule.
#create_list_tracking (chased)
- Runs when contractor parameter is not empty.
bTNLv1NewThingtime_tracking1when Search count for same contractor + shift equals0; initial values: contractor, shift,company_request_booleanfalse,send_booleanfalse.bTMCFScheduleAPIEvent →bTMBgwhen previous NewThing is not empty.bTMCPstores schedule id intoschedule_send_approve_texton the new/previous thing.
#Company approve and notes (chased)
- Single:
bTMSHsetsapprove_company_booleanandsend_paid_booleantrue. - Bulk:
bTMTZChangeListOfThings same two flags. - Contractors’ note Save
bTLhLwritesnotes_company_textand setssend_paid_booleantrue (same paid flag as Approve Save). - No reject /
not_approvedwrite path on the company time-tracking tab (bTLhndeleted).
#Fifteen-minute delay (chased)
bTKPa/bTKVs1: add user to show-contractor-list visibility and clear delay marker.- No create-list-tracking or send-tracking-approve side effects on this API — stale module-map wording that the delay “resumes” approve/create is wrong.
#Privacy roles (time_tracking1)
contractor_— view/search when CurrentUser is the contractor on the row.company_— view/search via shift ownership chain.admin_— view/search for admin role.everyone— no view/search.
Target: RLS + SECURITY DEFINER RPCs; Bubble ignore_privacy_rules on APIs must not become SPA bypasses.
#Option sets
| Option | db_value / notes | UI |
|---|---|---|
tracking |
tracking_your_time, time_tracking_reports |
Mode tabs |
break |
10_min…60_min (attrs.number 10…60) |
Break dropdown |
approve_type |
approved, waiting_for_approval; not_approved deleted |
Labels only |
#Residuals (decompose)
time_trackingoption set — display only, no values → dead.bTLhn/feedback_text— deleted → dead.bTHkxEndJobPopup — deferred to MOD-COMPANY-SHIFTS.bTMBTshift notify enqueue — answered structurally; consumer is TT-APPROVE-QUEUE.
#Decompose refs
tooling/cursor-impl-cycle/output/decompose/MOD-TIME-TRACKING.json — items TT-SCHEMA through TT-DELAY-JOB.