#Admin verification and ops
Module id: MOD-ADMIN
Doc profile: system
Platform admin console for TradeForce: role-gated /admin host, company and contractor verification lists, skill-rate editors on company detail, open/active shift inspection and edits, and invoice/payment-schedule recording. Target stack is React + Vite in app/ with Supabase Auth JWT + RLS/RPC under app/supabase/. Bubble page admin and its client workflows are the evidence for behavior; every persistent write must become server-authoritative (LOG-CLIENT-WRITES / MON-CLIENT-WRITES).
#Purpose
Platform admins need one console to verify companies and contractors, set skill rates, inspect and correct shifts, and mark time-entry payments as paid with notes. This module owns the /admin body inside the cabinet workspace, the tab query contract (adminnav / admintab), list/detail regions, approve and rate Save mutations, shift edit / stop-hiring, and admin payment confirm (including PostMark notify on edge). It does not own Header/SideBar chrome (MOD-SHELL), public auth (MOD-AUTH), contractor payment read UI (MOD-PAYMENTS), or Stripe Connect / KYC (MON-MARKETPLACE).
#Scope boundaries
| In scope | Out of scope |
|---|---|
/admin host, PageLoaded role gates, empty-tab default |
Workspace Header / SideBar / mobile admin menu links (MOD-SHELL) |
| Companies verified / not-verified lists + detailed company rates | Company shift creation product UI (MOD-COMPANY-SHIFTS) |
| Contractors verified / not-verified lists, detail, Approve + skill set | Contractor marketplace / Stripe Connect onboarding |
| Open / Active shifts list + detailed shift edit / stop hiring | Inventing schedulers beyond replacing Bubble ScheduleAPIEvent with the stack job approach |
| Payments tabs (previous / upcoming schedule / all invoices) + paid confirm | Contractor /contractor/payments read surface (MOD-PAYMENTS) |
Admin-only RLS/RPC for profile rates, approve, shifts, custom_rates, time-entry paid fields |
Shipping PostMark or service-role secrets to the browser |
#Actors
| Actor | System behavior |
|---|---|
| Admin | May remain on /admin; switches tabs; verifies companies/contractors; saves rates; edits/stops shifts; records admin paid amount/notes. |
| Company (signed in) | PageLoaded redirects off admin to the company shifts host. |
| Contractor (signed in) | PageLoaded redirects off admin to the contractor shifts host. |
| Visitor (signed out) | PageLoaded redirects to /. |
| Session client | Anon-key Supabase client + admin JWT; never ships connector keys. |
| Edge / RPC | Admin-authorized mutations and PostMark notify after payment confirm. |
#Admin composition
Admin is one cabinet page whose body regions switch on the tab query. Shell admin menu enters Shifts via admintab openshifts and Companies / Contractors / Payments via adminnav. Sub-tabs and detail tabs reuse the same param. Empty tab on load is filled with adminnav Shift (Bubble writes the option display). G-Shifts itself is visible only when tab matches openshifts or activeshifts — so the empty-tab default alone does not show the shifts list until Open/Active (or shell Shifts) is chosen.
Bubble evidence for composition and gates is listed under Traceability / IR evidence.
#Primary journeys
Journeys are reconstructed from admin page workflows (PageLoaded → ChangePage, ButtonClicked → Search/ChangeThing/NewThing/ScheduleAPIEvent/connector). Where a chain ends in an edge notify, the wire literals are named; PostMark template HTML itself lives outside the export.
#J1 — Open admin and land on a gated host
- An admin opens
/admin(with or withouttab); the page mounts inside the cabinet workspace with the admin body outlet. - If the session is missing, the app redirects to
/; if the signed-in role is company, it redirects to the company shifts host; if contractor, to the contractor shifts host. - When the user is an admin and
tabis empty, the app navigates to the same admin host withtabset to the Shift admin-nav default (Bubble option display). - Non-admins never see company, contractor, shift, or payment admin regions.
#J2 — Browse and open companies for verification
- Admin opens
/admin?tab=companiesand sees Verified Companies / Not verified Companies sub-tabs. - Verified lists company profiles that completed onboarding and have at least one skill rate column set; Not verified lists the inverse rate filter among the same role/onboarding set.
- Admin opens Details on a row and lands on
/admin?tab=detailedcompanywith that company as detail context. - Empty lists show “No result found”; query failures show a plain-language error in the companies region.
#J3 — Verify a contractor with skill set
- Admin opens
/admin?tab=contractors, switches Verified / Not verified (preserve wire value spellingveriedcontractorsfor verified), and opens a contractor into/admin?tab=detailedcontractor. - Verified lists contractor profiles that are active, onboarded, and approved true; Not verified lists the same role/active/onboarding set with approved false. On a not-yet-approved contractor, Admin chooses Approve, which opens the skill-set popup and loads the contractor into it.
- Admin confirms; the app persists approved true and the selected skill set via an admin-only server write, then closes the popup.
- Cancel closes the popup without approving; non-admin JWTs cannot set another user’s approved flag.
#J4 — Set company and contractor skill rates
- From detailed company, Admin sees Company rate and Contractor rate sections with exactly four pay-practice rows (Concrete Carpenter, Concrete Finisher, Concrete Laborer, Concrete Walls).
- Admin edits staging inputs and optional custom rate for an exact day (shift, existing rate, new rate).
- Admin chooses Save; the app creates a
custom_ratesrow when that path is used, updates the company profile rate columns, and appends the custom rate to the company’s list — all in one admin-authorized transaction. - Save failure shows a plain-language error; success resets inputs as the source does.
#J5 — Inspect and edit open or active shifts
- Admin opens
/admin?tab=openshiftsor/admin?tab=activeshifts, optionally filters by company, and opens a shift into/admin?tab=detailedshift. - Admin edits evidenced shift fields and chooses Edit; the app creates an in-app notification to the company that the shift was changed, persists via admin RPC, and when the shift date is still in the future cancels/reschedules the tracking job that Bubble named
shift. - Discard resets the editor and goes back without saving.
- Stop hiring confirms stop_hiring and sets shift type upcoming (clearing schedule tracking when present), and updates the related contractor’s dependability for a company-canceled-shift positive; cancel closes the popup only.
#J6 — Record an admin payment on an invoice row
- Admin opens
/admin?tab=paymentsand switches Previous payments / Upcoming payment schedule / All invoices. - Admin opens a row into the payment details popup and enters amount and notes.
- On confirm with amount present, the app sets admin paid amount, notes, and status paid on the time-tracking entry via admin RPC, then may notify via edge PostMark (
user-invitation-3, empty template model) using server secrets only. - Confirm with empty amount runs only when the payment-tabs report list has at least one row; it follows the list-update + in-app notification (“Your payment is on the way!”) path; Cancel closes without writing.
#Navigation contracts
| Label / trigger | Behavior |
|---|---|
| Shell Shifts (MOD-SHELL) | /admin?tab=openshifts (admintab) |
| Shell Companies / Contractors / Payments | /admin?tab= companies | contractors | payments (adminnav) |
Empty tab on admin PageLoaded |
Self ChangePage with adminnav Shift (display string) |
| Companies sub-tabs | State → verified_companies | notverifiedcompanies; host stays tab=companies |
| Contractors sub-tabs | State → veriedcontractors | notverifiedcontractors; host stays tab=contractors |
| Open / Active shift sub-tabs | /admin?tab=openshifts | activeshifts |
| Company / contractor / shift Details | detailedcompany | detailedcontractor | detailedshift |
| Payments sub-tabs | State → previous_payments | upcoming_payment_schedule | all_invoices; host stays tab=payments |
| Go-back on detail | Restores prior list tab per source go-previous / ChangePage |
#Vertical slices (decompose)
| Item | Product outcome |
|---|---|
| ADMIN-HOST | /admin in cabinet_workspace; PageLoaded gates; empty-tab → Shift default; region visibility by tab. Enabler for the journeys below. |
| ADMIN-COMPANIES | Verified / not-verified company lists; open detailed company. |
| ADMIN-CONTRACTORS | Verified (profiles.approved true) / not-verified (profiles.approved false) contractor lists; detail; Approve popup + server approve. |
| ADMIN-RATES | Four pay-practice rate editors + custom rate Save on detailed company. |
| ADMIN-SHIFTS | Open/Active lists; shift detail edit (+ company notification); stop hiring (+ dependability); schedule cancel/reschedule. |
| ADMIN-PAYMENTS | Three payment tabs; payment details popup; record paid + edge notify (empty-amount also gated on report count ≥ 1). |
#Side-effect contracts
| Trigger | Side effect | Owner |
|---|---|---|
| PageLoaded signed out | ChangePage → index / |
ADMIN-HOST |
| PageLoaded non-admin company / contractor | ChangePage → company / contractor shifts hosts | ADMIN-HOST |
PageLoaded admin + empty tab |
ChangePage self + tab = Shift display |
ADMIN-HOST |
| Open company / contractor / shift Details | tab → detailed* + selected row context |
ADMIN-COMPANIES / ADMIN-CONTRACTORS / ADMIN-SHIFTS |
| Confirm Approve | profiles.approved true + skill_set list replace (RPC) |
ADMIN-CONTRACTORS |
| Save rates | Insert custom_rates + update profile rate columns (RPC) |
ADMIN-RATES |
| Edit shift (future date) | In-app notification to company; update shift; CancelScheduledAPIEvent + ScheduleAPIEvent shift (→ stack job) |
ADMIN-SHIFTS |
| Stop hiring confirm | stop_hiring true; shift type upcoming; clear schedule tracking; bump contractor dependability (company_canceled_shift) |
ADMIN-SHIFTS |
| Confirm payment (amount present) | time entry admin_paid / notes / status paid; optional PostMark edge | ADMIN-PAYMENTS |
| Confirm payment (amount empty + report count ≥ 1) | List update + notification “Your payment is on the way!” + PostMark edge | ADMIN-PAYMENTS |
#Role gate matrix (PageLoaded)
| Rule | Predicate | Destination |
|---|---|---|
| Signed out | not logged in | / (index) |
| Non-admin company | logged in, role ≠ admin, role = company | Company shifts host |
| Non-admin contractor | logged in, role ≠ admin, role = contractor | Contractor shifts host |
| Empty tab fill | logged in, tab empty |
/admin with Shift default |
#Style and fidelity
Admin uses cabinet_workspace (section → container → workspace head from SHELL + surface-card body). Stamp data-bubble-id on the admin page root and G-admin outlet. Preserve list/table layout (not marketing cards), sub-tab labels including leading spaces on “ Not verified …”, empty copy “No result found” / “No data”, and detail labels (Dependability score, Company Name Rates, payment popup fields). 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.
#Failure modes
| Case | Expected behavior |
|---|---|
Signed-out or non-admin hits /admin |
Redirect away; no admin list data rendered. |
| Profile/session read fails on host | Plain-language error in the outlet; no silent catch. |
| Companies / contractors / shifts / payments query fails | Plain-language error in that region. |
| Approve / Save rates / shift edit / stop hiring / record payment rejected | Plain-language error; no partial success UI; non-admin JWT must fail at RLS/RPC. |
| Empty-amount payment confirm with zero report rows | Confirm empty-amount path does not run (report count gate). |
| PostMark / notify edge failure after paid write | Paid persistence may already be committed; surface notify failure without rolling back inventively unless RPC designs otherwise — do not expose upstream bodies. |
| Client-only Bubble ChangeThing ported as-is | Blocked: money and verification writes require server authority (LOG-CLIENT-WRITES / MON-CLIENT-WRITES). |
| Stripe Connect / payouts | Out of scope (MON-MARKETPLACE); do not invent KYC journeys from this page. |
#Needs human clarification
None. After catalog + inventory + sanitized-export chase for PageLoaded gates, adminnav/admintab values, company/contractor Search filters (including not-verified approve1_boolean equals false), Approve/Save/Edit/Stop-hiring/Payment workflows, ScheduleAPIEvent shift, and PostMark body literals (From, TemplateAlias user-invitation-3, empty TemplateModel), admin console behavior is encoded in the export. PostMark template HTML content remains outside the export (same wire alias as MOD-PAYMENTS cycle mail) but is not required to implement the call site. Server-authority migration is a stack obligation, not an open product question.
#Traceability
| Product concept | Target (route / component) | Bubble evidence |
|---|---|---|
| Admin page host | /admin, AdminPage / AdminHost |
bTHun0, bTHvk0 |
| Signed-out gate | roleGate → / |
Workflow bTKrc / action bTKrj → bTGYf |
| Non-admin role gates | roleGate → company / contractor hosts |
Workflow bTJeL / bTKmk → bTITd, bTKoP → bTHMg |
| Empty tab default | tabContract Shift |
Workflow bTKmq / action bTKmw |
| Companies region | AdminCompaniesPage |
bTHxi0, bTHwr0, sub-tabs bTHwD0 |
| Company detail + rates | CompanyRatesPanel |
bTHyA0, Save bTKZn / bTKaL |
| Contractors region + approve | AdminContractorsPage, ApproveSkillSetPopup |
bTIBH0, bTIBv0, element_condition:bTIBv0:1, bTICV0, bTIIT0, bTJKz |
| Shifts region + detail | AdminShiftsPage, ShiftDetailEditor |
bTIKT0, bTIMv0, bTMYL / bTMYS, Edit bTMcD / notify bTMeM |
| Stop hiring | StopHiringPopup |
bTMYr, confirm bTMZI / dependability bTMZT |
| Payments region + popup | AdminPaymentsPage, PaymentDetailsPopup |
bTKHz0, bTKKu0, bTMpJ / bTMqB, custom_state:bTKIM0:report_ |
| Admin nav / tab enums | tabContract |
adminnav, admintab |
| Pay practices (4 skills) | payPractices enum |
pay_practices |
| Custom rates entity | custom_rates |
custom_rate |
| PostMark notify | Edge admin-payment-notify |
external_ns:bTNMZ, actions bTNNo / bTNNp |
| Schedule tracking job | Stack job replacing Bubble schedule | bTMcN → API event bTMBT (wf_name shift) |
#IR evidence
#Module evidence roots
bTHun0— admin page (/pages/bTHuo0), G-admin body, all admin list/detail workflows.
Supporting segments: ir/slices/bTHuo0____root.json, ir/slices/bTHuo0__elements____remaining_children.json, ir/slices/bTHuo0__elements__bTHvp0__elements____remaining_children.json, ir/slices/bTHuo0__workflows____part_A.json, user types company, custom_rate, rates, option sets adminnav, admintab.
#Path-key ↔ inventory id aliases
| Path key / pointer | Inventory id | Notes |
|---|---|---|
/pages/bTHuo0 |
bTHun0 |
admin |
/pages/bTHuo0/workflows/bTKrn |
bTKrc |
PageLoaded not logged in |
/pages/bTHuo0/workflows/bTJeM |
bTJeL |
PageLoaded role ≠ admin |
/pages/bTHuo0/workflows/bTKmx |
bTKmq |
PageLoaded empty tab |
/pages/bTHuo0/workflows/bTKaN |
bTKaL |
Save rates |
/pages/bTHuo0/workflows/bTJLE |
bTJKz |
Confirm approve |
/pages/bTHuo0/workflows/bTMYN / bTMYX |
bTMYL / bTMYS |
Open / Active shifts tabs |
/pages/bTHuo0/workflows/bTMcU |
bTMcD |
Edit shift |
/pages/bTHuo0/workflows/bTMpL / bTMqN |
bTMpJ / bTMqB |
Payment confirm (amount / empty) — catalog ids bTMpJ / bTMqB |
/api/bTMBU |
bTMBT |
Scheduled API wf_name shift |
#Tab and option contracts (chased)
adminnav db_values: shift, companies, contractors, payments, detailedcompany, detailedcontractor, detailedshift (shift_type marked deleted).
admintab db_values: veriedcontractors (typo preserved), notverifiedcontractors, verified_companies, notverifiedcompanies, openshifts, activeshifts, all_invoices, upcoming_payment_schedule, previous_payments.
pay_practices: concrete_carpenter, concrete_finisher, concrete_laborer, concrete_walls (four rows).
Empty-tab fill (bTKmw) writes OptionValue display for adminnav/shift. G-Shifts visibility (element_condition:bTIKT0:0) is admintab openshifts or activeshifts only. Companies / Contractors / Payments groups key off adminnav companies / contractors / payments.
#List filters (chased)
- RG-Companies (
bTHwr0): Searchuserwith role company + onboarding; verified path further requires any ofcompany_rates_number/company_concrete_finisher_number/company_concrete_laborer_number/company_concrete_walls_numbernon-empty (filtered constraints). - RG-Contractors (
bTIBv0): role contractor +active__booleantrue +onboarding_booleantrue; verified Search addsapprove1_booleanequals true; not-verified Search (element_condition:bTIBv0:1, admintabnotverifiedcontractors) uses the same role/active/onboarding constraints withapprove1_booleanequals false (targetprofiles.approved = false). - RG-payments (
bTKIX0):time_tracking1with tab-state constraints on approve_company / send_paid / status paid as element conditions encode.
#Mutations (chased)
- Approve (
bTJLF):skill_set_list_custom_skill_setset_list from popupcustom.skill_;approve1_booleantrue. - Save rates (
bTKaR/bTKaT): NewThingcustom.custom_rate(shift, existed_rate, new_rate); ChangeThing company rate columns + add tocustom_rate1_list. - Edit shift (
bTMcDchain): NewThingcustom.notificationto company (bTMeM, text “Your shift "…" was changed”); ChangeThing shift fields; CancelScheduledAPIEvent; ScheduleAPIEventbTMBT(shift) when the shift date is still after now. - Stop hiring (
bTMZI): ChangeThing stop_hiring / shift type; ChangeListOfThings user dependability (bTMZT,dependability_scoup_number+positivescompany_canceled_shift). - Payment confirm with amount (
bTMpP):admin_paid___number,admin_notes_text,status_option_status_paid= paid; then connectorbTNNo. - Payment confirm empty amount (
bTMqBOnly-when: ElementParent amount is_empty and GetElementbTKIM0custom.report_count ≥ 1): list update (bTMqR) + NewThing notification text “Your payment is on the way!” (bTNLe) + connectorbTNNp. - Both connectors: body From
amanda.gasman@tradeforce.dev, To contractor email, TemplateAliasuser-invitation-3, TemplateModel{}.
#Privacy & authorization
Target: only profiles.role = admin may read verification lists and mutate other users’ approved/rates, shifts stop/edit, custom_rates inserts, and time-entry admin paid fields. SPA uses user JWT + RLS/RPC; PostMark secrets only on edge. Bubble privacy_role:* rows for these types are answered structurally in decompose → DATA-MODEL RLS, not separate journey items.
#Naming map (idiomatic highlights)
| Target | Bubble evidence |
|---|---|
profiles.role / approved / onboarding_complete / active |
role_option_role, approve1_boolean, onboarding_boolean, active__boolean |
profiles.company_* / contractor_* rate columns |
company_*_number, contractor_*_number |
custom_rates |
custom_rate (existed_rate_number → existed_rate, new_rate_number → new_rate) |
time_tracking_entries |
time_tracking1 (admin_paid___number → admin_paid_amount, admin_notes_text → admin_notes) |
pay_practices enum |
option pay_practices db_values |
Note: inventory user_type rates is an evidence segment; do not invent a live rates table from deleted fields — skill rates live on profiles + custom_rates as chased above. User_type company remains a segment label; admin company lists Search user with role company.