β€”

πŸ‘‹ Welcome to GDQ Leads

GDQ Leads is the in-house CRM for managing leads, jobs, contractors, and service areas across all offices. This help center walks through every feature, with live UI previews you can compare to what's on your screen.

The system is split into a small number of pages, plus shared panels (SMS & Tasks) that slide in from the left on any page. You don't need to memorize anything β€” most flows are click-driven.

The four main pages

  • Leads β€” every prospect from every source. Where the day-to-day phone work happens.
  • Jobs β€” booked appointments. Created from Leads or directly. Has its own statuses and webhooks.
  • Contractors β€” directory of who covers what areas + job types. Owner/viewer access.
  • Service Areas β€” geographic pins (50-mile radius each) that contractors opt into. Owner/viewer access.

Shared panels (every page)

  • πŸ“± SMS β€” every CTM-routed SMS conversation, with reply, templates, and STOP/UNSUBSCRIBE handling.
  • πŸ“‹ Tasks β€” to-do items assigned to you or your office, optionally linked to a lead or job.

Where to start

If you're new to the system: read the Leads dashboard chapter first β€” that's where you'll spend most of your time. After that, the chapters are independent; jump around as needed using the sidebar.

πŸ”‘ Login & roles

Every action in the system is gated by your role. Knowing yours tells you what you can and can't do.

Roles

RoleTypical userWhat they do
OwnerYou / leadershipEverything. The only role that can manage Contractors, Service Areas, and the Control Panel.
AdminOffice manager / dispatcherFull Lead + Job management, status changes, webhooks, K-confirm, automations log access (for the Jobs page).
AgentCSR on the phonesWorks leads end-to-end (claim, status, notes, SMS). Can't access Jobs, Contractors, or Service Areas pages.
ViewerAuditor / read-only stakeholderSees data on every page they're allowed onto, but cannot edit. Can see Contractors and Service Areas (read-only).

Page access by role

PageOwnerAdminAgentViewer
Leadsβœ“βœ“βœ“read
Jobsβœ“βœ“readread
Contractorsβœ“readreadread
Service Areasβœ“readreadread
Helpβœ“βœ“βœ“βœ“
Control Panelβœ“βœ—βœ—βœ—
πŸ’‘
Pages are open to every logged-in role for viewing. Write actions still go through API role gates: only admin+ can create/edit jobs, contractors, and service areas. Only owner can delete jobs.

Per-office Jobs feature toggle

Owner CP β†’ βš™οΈ Office Settings has a πŸ”§ Jobs Feature switch per office. When off for an office:

  • Users assigned only to that office can't open /jobs, /contractors, or /service-areas β€” they redirect back to /leads.
  • Top-nav links to those pages are hidden for them.
  • Reports modal hides the Jobs tab for them.
  • The office's existing jobs are excluded from /api/jobs queries, so they vanish from the dashboard until you flip the toggle back on (rows stay in the DB intact).
  • The office's converted leads keep the legacy "πŸ”§ Mark as Opened in Workiz" flow.

Use this when an office isn't ready for the Jobs flow β€” currently set up so we can run Jobs on Garage/Sliding/General while Joes Doors FL stays on the manual flow.

Office assignment

Each non-owner user is assigned one or more offices (e.g. Sliding, Garage, General). Office filters everything you see:

  • You only see leads tagged with one of your offices.
  • Jobs you create default to your office.
  • Tasks scoped to "My Office" filter to your assigned offices.

Owner sees every office. Multi-office users get a comma-separated assignment (managed in CP β†’ Users).

🧭 The topbar

The horizontal strip at the top of every page. Same layout on Leads, Jobs, Contractors, and Service Areas.

Live UI Preview Β· Topbar
GDQ GDQ Leads Leads Jobs Contractors Service Areas Help
πŸ‘‘ OWNER CSR: Omri πŸ“± SMS πŸ“‹ Tasks 3 πŸ“Š Reports πŸ‘₯ Users βš™οΈ Control Panel Sign Out

What each item does

  • Page nav (Leads / Jobs / Contractors / Service Areas / Help) β€” the active one is highlighted blue. Items hidden if your role can't access that page.
  • Role badge β€” color-coded (orange owner, blue admin, green agent, gray viewer) so you can tell at a glance what permissions you have.
  • πŸ“± SMS β€” opens the SMS slide-in panel. Pulses red when there are unresolved inbound messages.
  • πŸ“‹ Tasks β€” opens the Tasks slide-in panel. Pulses red with a count badge when you have tasks due today or overdue.
  • πŸ“Š Reports β€” owner-configured email reports + on-demand summary.
  • πŸ‘₯ Users β€” directory of users in the system; admins+ can edit, viewers see read-only.
  • πŸ“‹ Timeline β€” global activity feed (every status change, claim, etc.) β€” admins/viewers/owners only.
  • πŸ“‹ Audit Log β€” owner-only. Every system event for compliance/forensics.
  • βš™οΈ Control Panel β€” owner-only. The big settings hub β€” everything from sources to automations to recurring tasks.
  • πŸ”• / πŸ”” Notifications β€” toggles browser desktop notifications for new leads.
  • Sign Out β€” clears your session and returns to the login screen.

🏒 Offices explained

"Office" is the org-bucket every lead, job, and most settings get tagged with. It's the most important concept after roles.

An office is just a name like Sliding, Garage, General, or Joes Doors FL. It doesn't represent a physical location β€” it represents a routing bucket. Owners manage the list in CP β†’ Offices.

What office tags do

  • Visibility β€” non-owner users only see leads/jobs whose office matches one of their assigned offices.
  • Phone & email routing β€” each office has its own CTM number (for SMS automations) and SMTP credentials (for email automations). Set in CP β†’ Office Settings.
  • Webhook payloads β€” outbound webhooks (Workiz / Deposit / Cancelled / New Lead) include the office field so downstream tools can branch by it.
  • Automation conditions β€” every automation rule can scope to one or more offices.
  • Online Booking embeds β€” each embed maps submissions to a specific office.

How an office gets onto a record

  • Inbound lead webhooks β€” the source's webhook config sets the office (or defaults to General).
  • Manual lead create β€” admins/owners can set it; agents inherit their first assigned office.
  • Online Booking β€” every embed has a fixed office.
  • New job β€” the New Job modal has an Office dropdown at the top; defaults to your assigned office.
πŸ’‘
Office is frozen at create time on jobs. If a lead's office changes later, existing jobs aren't retroactively re-tagged.

πŸ“‹ Leads dashboard

The default landing page. A filterable list of every lead from every source.

Live UI Preview Β· Leads dashboard
Office: All β–Ύ Status: All β–Ύ Source: All β–Ύ Urgency: All β–Ύ πŸ”€ Transferred ⚑ BookBack πŸ“… Last 7 days β–Ύ
TimeNamePhoneSourceStatusUrgencyOffice
10 min agoBig Lou+1305β€’β€’β€’β€’β€’β€’β€’Facebook AdsFreshHighSliding
2 h agoCarmen Diaz+1786555β€’β€’β€’β€’YelpNeeds a Follow UpMediumGarage
YesterdayMike Patel+1954555β€’β€’β€’β€’Google AdsConvertedLowGarage
2 days agoSarah Kim+1305555β€’β€’β€’β€’Online Booking ToolNot Yet ScheduledMediumSliding

Columns

Office Β· Time Received Β· Full Name Β· Phone Β· TZ Β· Metro Β· Source Β· Status Β· BookBack AI Β· Next Contact Β· Actions.

  • Full Name β€” capped at 140px with ellipsis truncation. Hover to see the full name in a tooltip. Frees horizontal space for the Metro column.
  • Metro β€” closest major US metro derived from the lead's ZIP prefix (e.g. "Atlanta, GA" / "Los Angeles, CA"). ~150 ranges covering all major USPS Sectional Center Facilities. Empty for unmapped prefixes.

How rows are sorted

  1. Overdue follow-ups first (Needs a Follow Up + next-contact in the past)
  2. Fresh leads with no scheduled callback
  3. Not Yet Scheduled / Needs a Follow Up with a future callback
  4. Converted
  5. Irrelevant
  6. Inside each tier: most recent tr (time received) first

The filter row

  • Office / Status / Source / Urgency β€” multi-select dropdowns. Pick any combination.
  • πŸ”€ Transferred β€” toggle to show only transferred leads (yellow accent).
  • ⚑ BookBack β€” toggle to show only BookBack-AI-reactivated leads (cyan accent).
  • Date range β€” preset (Today / Last 48h / Last 7 / Last 30 / This month / etc.) or custom range.
  • Search β€” fuzzy match across name, phone, email, campaign, ZIP.

Opening a lead

Click any row to open the right-side detail panel. The panel slides in over half the table; the lead's full info is editable inline (where your role allows it). Press Esc or click the βœ• to close.

Pagination

The table loads 25 rows at a time by default. The bar below the table has Prev/Next and shows "21–25 of 478 leads." Filters update the total. There's no infinite scroll.

Real-time updates

The page polls for new leads every few seconds. When a fresh lead arrives:

  • It appears at the top of the list
  • The page title flashes a count: (2) Leads β€” GDQ
  • If you've enabled browser notifications (πŸ”• button β†’ πŸ””), you get a desktop ping

🚦 Lead statuses & urgency

Every lead has a status and a derived urgency. The urgency drives both the row sort order and (in a moment) the visual color.

The five statuses

StatusMeansUrgency
FreshBrand new β€” never been worked. Default for incoming leads.High
Not Yet ScheduledMade contact, didn't book yet. Use the next-contact timer to schedule a callback.High when due, Medium otherwise
Needs a Follow UpActive follow-up needed (left voicemail, awaiting reply, etc.).High when due, Medium otherwise
ConvertedBooked into a job. Closing.Low
IrrelevantDisqualified (wrong service, prank, can't service the area, etc.).Low β€” fires the Cancelled Leads webhook on transition

Urgency rules

  • High β€” Fresh, OR (NYS / Needs FU with next_contact ≀ now)
  • Medium β€” NYS / Needs FU with a future next_contact
  • Low β€” Converted or Irrelevant

Urgency is computed live, not stored. The same row with a 9am callback shows as Medium at 8:55am and High at 9:05am β€” the page recomputes on every render.

Changing status

  1. Open the lead detail panel (click the row).
  2. The Status field is a dropdown β€” pick the new value.
  3. For Not Yet Scheduled or Needs a Follow Up: a date+time picker appears so you can set the next callback.
  4. For Irrelevant: a reason field appears (mandatory). Goes into the timeline + the Cancelled Leads webhook payload.
  5. Status change saves on blur β€” no Save button.
⚠️
Moving a lead to Irrelevant fires the Cancelled Leads webhook (if configured). It's not a simple status change β€” it's a "this lead is dead" event. Use it deliberately.

πŸ“ž Claiming a Fresh lead

Fresh leads have a hidden phone number until someone "claims" them. This prevents two CSRs from calling the same lead.

The claim button

On a Fresh lead, the phone field is blurred and replaced with a "πŸ“ž Show Number & Claim Lead" button. Clicking:

  1. Reveals the phone number for everyone (the blur clears)
  2. Stamps the lead with your name + timestamp ("Claimed by Omri")
  3. Adds a timeline entry: Lead claimed by Omri
  4. Other agents see a "πŸ“ž Omri" badge next to the phone

The claim is informational β€” it doesn't lock the lead. If two CSRs both click within seconds, the second click wins and the timeline shows both events. The expectation is that you call the lead immediately after claiming.

What unclaims it

A claim auto-clears when the lead leaves Fresh (status changes to NYS, Needs FU, Converted, etc.). The phone unblurs for everyone at that point.

Other agents' claims

You can see who claimed each Fresh lead from the dashboard β€” but the phone stays blurred to you until you claim it yourself. This is intentional friction so the claim is meaningful.

⏱ Snooze sequences

Per-source rules that auto-set the next-contact time after each call attempt. So you don't have to think about "when should I retry?".

How it works

Each source (Facebook, LSA, Yelp, etc.) has a configured sequence of intervals. There are three built-in defaults:

  • Standard (Facebook / Google / LSA / Website / Nextdoor / Manual) β€” 22 attempts over 17 days. Cadence: 4 calls in first 12h + 2 in next 12h on day 1+2, repeat for day 3+4, then daily at varying clock hours for 7 days, then 3 calls every 48h.
  • Aggressive (Angi / Thumbtack / Yelp) β€” 14 attempts: 5min Β· 10min Β· 20min Β· 30min Β· 1h Β· 2h Β· 3h Β· 12h Β· 1d Β· 1d Β· 1d Β· 2d Β· 2d Β· 20d.
  • BookBack AI β€” 14 attempts: 15min Γ— 7 then 8h Γ— 7.

When you log a call attempt on a lead (the dial pad button on the detail panel), the system reads the lead's source, looks up the right interval based on call count, and sets next_contact = now + interval. The lead drops off your "due now" list and reappears at the right time.

adjustToCallingHours() shifts any computed snooze that would land outside the configured calling hours into the next valid window β€” so a 3 AM slot moves to ~9 AM automatically.

After the cap (22 attempts on Standard, 14 on the others) the next "Tried to Call" press surfaces an Irrelevant prompt instead of scheduling another snooze.

Where to configure

  1. Owner CP β†’ General Settings β†’ Snooze Sequences
  2. Each source has its own row. Click Edit to set intervals.
  3. Intervals are minutes; a small editor lets you add/remove steps with units (min/h/d).
πŸ’‘
If a source has no snooze sequence configured, calls just bump the count β€” they don't change next_contact. You'd need to set the next contact manually.

πŸ“¦ Archive & restore

Owner-only. Soft-deletes a lead. The data stays in the database; it's just hidden from the main dashboard.

  1. Open the lead, click the πŸ“¦ Archive icon in the detail panel header.
  2. Confirm. The lead disappears from the main list immediately.
  3. To restore: topbar β†’ πŸ“¦ Archived Jobs (also lists archived leads). Click β†’ Restore.

Archived leads keep their full timeline + history. Permanent deletion is available too (?hard=true on the API call) but is not exposed in the UI.

⚑ BookBack AI reactivation

When a lead that's already in the system is hit again by BookBack AI, the existing record is "reactivated" β€” not duplicated.

What triggers it

BookBack posts to /webhook/leads with source=bookback. The system matches the incoming phone (last 10 digits, ignoring formatting) against existing leads. If it finds a match:

  • The existing lead is updated, not duplicated
  • Status resets to Fresh (calls counter zeroed)
  • Urgency forced to High
  • BookBack's notes are appended to the existing notes (with a timestamp header)
  • A timeline entry: ⚑ Reactivated by BookBack AI β€” was Converted (Yelp)
  • A bookback_calls row is recorded (with playbook score, objection, recording URL when available)

Filtering BookBack leads

The dashboard filter row has a ⚑ BookBack toggle (cyan). Click to show only leads where bookback=true.

πŸ” Duplicate cleanup

Owner tool for cleaning up phone-or-FB-ID collisions across leads. Reachable from the topbar πŸ” Check Duplicates button.

What it finds

  • Leads with the same last-10 phone digits
  • Leads with the same Facebook lead ID (rare since cross-source dedup was removed)

What it lets you do

  1. Open the duplicates modal β€” groups of matched leads appear, sorted newest first.
  2. For each group: choose which lead to keep (radio button). The others get archived; their timelines copy onto the keeper as "Merged from L-XXXX."
  3. Click Resolve per group, or Resolve all to apply your selections in batch.
πŸ’‘
The system itself does NOT silently dedupe inbound webhooks (we removed that earlier β€” it was hiding leads). Duplicates land in the dashboard normally; the cleanup tool is the deliberate place to merge them.

πŸ›  Jobs dashboard

Booked appointments. Open to every logged-in role for viewing; write actions still gated to admin+. Hidden entirely for users whose offices all have the Jobs feature toggled off (CP β†’ Office Settings).

Live UI Preview Β· Jobs dashboard
Status: All β–Ύ Type: All β–Ύ Source: All β–Ύ 🚨 Needs Attention (4) + New Job
Job IDOfficeDate & WindowClientAddressTypeStatusReb?K?
πŸ“… Today Β· Thursday, Apr 30 Β· 2 jobs
JOB-3601SlidingApr 30 Β· 2pm–4pmMike Patel123 Main St, Hialeah FL 33012RepairIPπŸ”΅ ASK FOR UPDATEβ€”K
JOB-3602GarageApr 30 Β· 4pm–6pmCarmen Diaz456 Oak Ave, Miami FL 33133InstallSubmittedβ€”βœ“ K
πŸ“… Tomorrow Β· Friday, May 1 Β· 3 jobs
JOB-3603SlidingMay 1 Β· 9am–11amSarah Kim789 Pine Rd, Coral Gables FL 33134RepairDEPπŸ”K

Columns

  1. Job ID β€” formatted as JOB-3542 and up. Counter starts at 3542 (business choice).
  2. Office β€” the org bucket. Frozen at create time.
  3. Date & Window β€” the appointment time range. The list groups by day with a thicker divider; today's divider is highlighted blue.
  4. Client β€” first + last name.
  5. Phone β€” the customer's number.
  6. Address β€” the job site address (truncated).
  7. Job Type β€” pill (Repair, Install, etc.).
  8. Company β€” the brand name being represented to the customer.
  9. Contractor β€” the assigned contractor, OR a blinking πŸ” FIND CONTRACTOR button if unassigned (hidden for Done/Cancelled).
  10. Status β€” short-code pill (S / IP / DEP / P / D / C). Hover for the full name. May show an attention badge below.
  11. Rebooked? β€” πŸ” if the job has been rebooked at least once.
  12. K? β€” green K button (or βœ“ K if confirmed). See K-confirmation.
  13. Created β€” relative time ("3 days ago").

Filter row

  • Search β€” fuzzy match across customer name, phone digits, address, job number (display_id), and contractor name.
  • Multi-filter dropdowns β€” Status / Job Type / Job Source / Office / Contractor. Each accepts multiple selections. The Contractor dropdown has a built-in search and a β€” Unassigned β€” entry.
  • Sort β€” within each date group, choose Status (Submitted β†’ IP β†’ Pending β†’ Deposit β†’ Done β†’ Cancelled), Time, or Contractor A–Z. Date groups themselves stay chronological.
  • Date filter β€” All Dates / Today only / Today Β· -3D / +7D (default) / This week / This month / Next month. Past dates outside the window are hidden so the page doesn't pile up.
  • 🚨 Needs Attention (red) β€” toggles to attention-only view. Bypasses the date filter so flagged jobs show regardless of when. Forces every date row expanded.
  • πŸ” Rebooked (blue) β€” toggles to rebooked-only view (jobs with rebooked_at set).
  • βœ• Clear filters β€” appears whenever any filter is active. Resets search, multi-filters, attn / rebooked toggles, and the date filter back to "All Dates."

Sorting & date dividers

Rows sort chronologically by appointment time (date_start ASC, time_start ASC), then by status priority within a day. Above each new date a thicker divider says e.g. πŸ“… Today Β· Thursday, Apr 30 Β· 3 jobs. Empty days don't appear.

Collapsible dates

Each divider has a β–Ό/β–Ά toggle on the left. Past dates auto-collapse by default; today and future dates start expanded. Click the arrow to flip an individual date. When a collapsed date contains an attention-worthy job, a blinking 🚨 NEEDS ATTENTION pill surfaces on the divider so it stays visible.

πŸ“‹ EOD Reminders + πŸŒ… Morning Reminders

Each date divider also has two blue buttons on the right:

  • πŸ“‹ EOD Reminders β€” opens a modal with one row per contractor scheduled that day. Each row has a contractor-specific Google Static Map (numbered red pins) and the long-form text β€” Job#, Customer, Phone, Address, Job Type, Notes, Date+Time β€” labelled "Reminder for Tomorrow." Per-row Copy Image / Copy Text buttons. Use the night before for next-day dispatch.
  • πŸŒ… Morning Reminders β€” same modal structure, short-form text ("1. ***6AM-8AM*** address"), labelled "Reminder for Today." Use the morning of for the day's reminder.

Both flows pre-resolve any job missing lat/lng via the Places find-from-text endpoint, so Static Maps never needs the Geocoding API. Maps Static API must be enabled in Google Cloud Console for the configured key.

The "Needs Attention" filter

The 🚨 button at the top pulses red when there's at least one row needing action. The button shows the live count: 🚨 Needs Attention (4). Click to filter to only those rows (which also forces every date open). A row needs attention when:

  • Deposit + expected-completion date in the past β†’ πŸ”΄ UPDATE NEEDED (clickable β€” copies a "What's the update please?" message with the deposit-collected date for that contractor)
  • Pending + follow-up time passed β†’ 🟣 FOLLOW UP NEEDED
  • In Progress + (job end + 2h) passed β†’ πŸ”΅ ASK FOR UPDATE (clickable β€” copies the same "What's the update please?" template with the scheduled time window)
  • Active jobs without a contractor β†’ 🟑 FIND CONTRACTOR (Submitted / IP / Deposit / Pending only β€” Done/Cancelled with no contractor are historical and don't blink)

All four use the same blinking pulse so they breathe in sync. Clicking ASK FOR UPDATE or UPDATE NEEDED copies a ready-to-paste contractor message:

πŸ””Update NeededπŸ””
🏷️Job #21434 - Emily Harrison
πŸ“ 1379 Algiers St, Pt Charlotte, FL 33980
πŸ•’ 5/3 9AM-11AM - DUE
What's the update please?

βž• Creating a job

Click + New Job in the topbar. A wide modal opens with a 2-column form.

Step-by-step

  1. Pick the office. Office sits at the top of the modal, defaulted to your assigned office. It scopes everything else.
  2. Fill in client info. First name + phone are required. Phone goes through US validation β€” placeholders like 1231231234 are rejected.
  3. Type the address. Google Places autocomplete suggests as you type. Pick a suggestion to auto-fill city/state/ZIP behind the scenes. The mini-map in the right column zooms to the address.
  4. Pick the service area. The 5 closest service-area pins appear as radio cards on the right. Click one β€” its area code + office address auto-fill.
  5. Pick the job type and source. Both dropdowns. Required.
  6. Pick the contractor (optional). The dropdown is filtered to contractors who cover the chosen service area AND handle the chosen job type. If you click the dropdown without picking area + type first, a tooltip points at what's missing.
  7. Set the date & time window. Date pickers + 15-minute-step time pickers. The end time auto-fills as start + 2 hours.
  8. Click Create Job. The job lands as Submitted, gets a JOB-#### ID, fires the Workiz webhook, fires the BookBack-jobs webhook, fires any matching job_created automations.

Outside-click protection

Clicking outside the modal (on the dark backdrop) doesn't close it instantly β€” you get a "Close without saving?" confirm so a stray click doesn't wipe a half-filled form.

🚦 Job statuses (deep dive)

Each status has different required fields when transitioning to it, and each fires (or doesn't fire) different webhooks.

StatusRequired fields when enteringWebhook fires
SubmittedNone β€” the default after create.workiz_webhook_url + bookback_jobs_webhook_url (on creation)
In ProgressNote (optional)None
DepositNote (mandatory) + Expected completion date + Deposit amountdeposit_webhook_url
PendingNote (mandatory) + Follow-up date+timeNone
DoneClosing total + Parts totalworkiz_webhook_url
CancelledCancellation reason (mandatory)cancelled_jobs_webhook_url

Re-opening a Cancelled job

Cancelled jobs aren't dead. The status buttons stay visible in the detail panel; clicking another status (e.g. back to Submitted) re-opens the job and fires the appropriate webhook for the new status. A warning notice appears: "⚠ This job is cancelled. Picking another status will re-open it and fire the webhook."

The detail panel attention banner

If a job is in attention state when you open it, the top of the detail panel shows a red/purple/cyan banner explaining what's wrong. Disappears once you take action. The πŸ”΄ / πŸ”΅ banners are clickable and copy a "What's the update please?" message to the clipboard for sending to the contractor.

Schedule changes go to the timeline

Editing a job's date_start / time_start / date_end / time_end from the detail panel writes a πŸ“… Schedule changed: 2026-05-02 09:00–11:00 β†’ 2026-05-03 14:00–16:00 entry to the job timeline so anyone reviewing later sees what moved and when. The πŸ” Rebook flow logs its own πŸ” Rebooked entry so the two paths show up distinctly.

βœ… K-confirmation

"K" stands for Konfirmed (or Kept-the-job, depending who you ask). It's a one-click signal that the assigned contractor accepted the appointment.

Why it exists

Sending a job to a contractor is one thing β€” getting them to confirm they're actually taking it is another. The K column gives the dispatcher a single visual marker per row: green-K-button for "still waiting" vs βœ“-K for "confirmed."

The flow

  1. Click the green K button in the row.
  2. A small dialog opens: "Did Mike Hernandez accept the job at 123 Main St on Apr 30, 2:00pm?" with a contractor dropdown.
  3. If the assigned contractor accepted: leave the dropdown alone, click Mark as Accepted.
  4. If a different contractor took it: pick them from the dropdown. Confirming both flips the K AND reassigns the job (and re-snapshots Profit Share).
  5. If no contractor is assigned yet: the dialog asks "Which contractor accepted?" β€” pick one and confirm.

The K button flips to βœ“ K and a timeline entry is written: βœ… K-confirmed β€” Mike Hernandez accepted the job.

Un-K

Click βœ“ K β†’ "Clear K-confirmation?" β†’ confirm. The flag clears, the row needs K again. Useful if a contractor confirms then backs out.

K resets on rebook

When a job is rebooked, the K-confirmation always clears β€” even if the rebook didn't change the contractor. The reasoning: the schedule changed, so the contractor's prior acceptance is no longer authoritative.

πŸ” Rebooking

Mark a job as rebooked when the appointment moves OR the contractor changes. Click the πŸ” Rebook button in the detail panel header.

The dialog

Two checkboxes:

  • Change date / time β€” when checked, reveals new start date + time + end date + time pickers (15-min steps). Pre-filled with the current values.
  • Change contractor β€” when checked, reveals a contractor dropdown filtered by area + job type. Pre-selected to the current contractor.

You can check either, both, or neither. "Neither" still flags the job as rebooked (timeline marker only) β€” useful if you just want a paper trail that something shifted out-of-band.

What happens on save

  • The rebook timestamp + user are stamped (rebooked_at / rebooked_by).
  • Date/time changes apply.
  • Contractor change applies AND profit share re-snapshots from the new contractor.
  • K-confirmation clears.
  • Timeline marker: πŸ” Rebooked β€” new window May 5 9am Β· new contractor: Joe Smith Β· K-confirmation cleared.
  • The πŸ” icon appears in the Rebooked? column on the dashboard.

πŸ” Find Contractor

For unassigned jobs, the Contractor column shows a yellow blinking πŸ” FIND CONTRACTOR pill instead of "β€”".

Click it (or open the job and use the Edit flow on the contractor field) to get a small dialog with a dropdown of contractors who cover the job's service area + handle the job type. Pick one β†’ assigned. The pill goes away, the contractor name appears in the column, profit share snapshots, and a timeline entry is written.

This isn't K-confirmation β€” it's just assignment. The K button stays in its "needs K" state until you separately mark it.

For Done and Cancelled jobs, the pill is suppressed (replaced with quiet "Unassigned" italic text) since the work is historical.

πŸ’Ό Profit Share

Admin-only. The contractor's percentage of the job's profit, snapshotted onto each job at create/assign time so future contractor edits don't rewrite past financials.

Where it's defined

Each contractor has four fields (set in the Contractors page):

  • Profit Share % (default)
  • Secondary Profit Share %
  • Zipcodes for Secondary Profit Share β€” comma-separated
  • Job Sources for Secondary Profit Share β€” comma-separated (e.g. "Yelp, Angi, BookBack AI"). Case-insensitive.

The secondary % wins when either the job's ZIP matches one of the secondary ZIPs OR the job's source matches one of the secondary sources. Otherwise the default % applies.

How the snapshot works

  1. Job created OR contractor assigned/changed OR job's postal code or source edited β†’ server resolves the % at that moment.
  2. Stored on the job row (jobs.profit_share).
  3. Future edits to the contractor's % fields don't touch existing jobs.
  4. The detail panel shows the stored % to admins/owners only (under the Contractor Info section).

The New Job modal also shows a live-computed preview as you pick a contractor + address, so you can see what would land before saving.

πŸ’‘
The Contractor Info section shows the contractor's name to everyone, but the % only to admins/owners.

πŸ‘· Contractors page

Open to every logged-in role for viewing; only owner can edit. The directory of who covers what areas + handles what job types.

Layout

  • Left sidebar β€” search box, industry filter pills, scrollable contractor list. Each row shows the contractor's name, primary contact, area count, industry pills, an Active toggle, and Edit/Areas buttons.
  • Right map β€” every service area as a colored pin. Coverage density coloring: red = 0 contractors, yellow = 1, green = 2+. Hover a pin β†’ list of contractors covering it. Click a contractor row β†’ only their areas highlight blue, others dim.

Adding a contractor

Click + Add Contractor in the topbar. Modal fields:

  • Name (required) β€” the company name
  • Email (Reports) β€” where reports go
  • Contact Person + Phone β€” primary contact info
  • 2nd Contact Person + 2nd Phone β€” optional backup
  • Notes β€” free-form
  • Job Types (required) β€” checkbox list. The contractor is matched to jobs only if their job type matches.
  • Service Areas (optional β€” can edit later) β€” checkbox list with a search box. You can save a contractor with no areas yet and wire them up via the πŸ—Ί Areas map editor afterwards.
  • Profit Share % + Secondary % + Zipcodes for Secondary + Job Sources for Secondary β€” see Profit Share
  • Active checkbox β€” uncheck to take the contractor out of New Job dropdowns without deleting them

Inline Active toggle

Each row has a small "βœ“ Active" / "β—‹ Off" pill. Click without opening the modal to toggle β€” useful for quickly pausing a contractor.

πŸ—Ί In-place service area editing

A faster way to add/remove a contractor's service areas without opening the full edit modal.

The flow

  1. Click the πŸ—Ί Areas button on a contractor row.
  2. A blue banner appears above the map: "Editing coverage for Mike Hernandez Β· Click pins to toggle Β· Done."
  3. The contractor's covered pins flip to vivid blue. Uncovered pins show as a clear, clickable gray.
  4. Click any pin to add/remove that area. The change saves immediately (optimistic UI; rollback on failure).
  5. Click Done on the banner β€” or click any other contractor β€” to exit edit mode.

Hover popovers and the area InfoWindow are suppressed during edit mode so clicks land cleanly. Profit share doesn't auto-recompute (areas don't drive that β€” ZIPs do).

πŸ“₯ Contractor bulk upload

Upload an Excel/CSV with up to 1,000 contractor rows at once. Click πŸ“₯ Bulk Upload in the topbar.

The columns

ColFieldNotes
AContractor NameRequired. Used for upsert match.
BContact Person
CPhone
D2nd Contact PersonOptional
E2nd PhoneOptional
FEmail (reports)
GProfit Share %0–100
H2nd Profit Share %0–100
IZipcodes for 2nd %Comma-separated
JJob Sources for 2nd %Comma-separated (e.g. "Yelp, Angi"). Case-insensitive.
KNotesOptional, free-form

Upsert by name

Names are matched case-insensitively. Existing contractors get their contact info / profit-share fields refreshed; industries and service areas are NEVER touched on update so manually-curated coverage survives a re-upload. Brand-new names insert as fresh rows with empty industries + areas.

What new rows need next

Bulk-uploaded contractors don't have job types or service areas yet β€” they won't appear in New Job matching. Open each one and tick the right job types + service-area pills to wire them up. The in-place areas editor is the fastest way.

πŸ“ Service Areas page

Open to every logged-in role for viewing; only owner can edit. The 50-mile-radius pins that contractors opt into.

Layout

  • Left sidebar β€” search box + a sortable list of areas. Each row shows label, city/state/ZIP, area code.
  • Right map β€” every area as a blue dot. Hover or click a dot β†’ details popover with city/state/ZIP + the count of contractors covering it + a Coverage by Job Type breakdown.

Adding an area

  1. Click + Add Service Area.
  2. Enter a label (free-form, e.g. "Hialeah") and the main ZIP code (5 digits).
  3. Click the Lookup button β€” it geocodes the ZIP through Google Places to derive city, state, lat, and lng.
  4. Optionally fill in Main Area Code + Office Address. These appear on the New Job modal when this area is picked.
  5. Save.
⚠️
If Main Area Code or Office Address need to change, edit them HERE β€” they're read-only inside the New Job modal so dispatchers can't accidentally drift them per job.

πŸ“₯ Service Area bulk upload

Excel/CSV upload of up to 500 areas. Click πŸ“₯ Bulk Upload.

The columns

  • A β€” Service Area (label)
  • B β€” Main Zipcode (5 digits)
  • C β€” Main Area Code (optional)
  • D β€” Office Address (optional)

Each ZIP is geocoded via Google Places to derive city, state, lat & lng. Rows with errors are listed at the end of the upload β€” fix them in the spreadsheet and re-upload. Duplicate labels are refused.

Outside-click protection

The bulk upload modal asks "Close Bulk Upload?" if you click outside it after picking a file. Same pattern as every other modal in the system.

πŸ“± SMS panel

Slide-in inbox for every SMS routed through CTM. Available on every page via the πŸ“± SMS button in the topbar.

Live UI Preview Β· SMS panel (slid in from the left)
πŸ“± SMS Inbox 3
Big Lou2
via 305-555-0111
← Hi, can you come by tomorrow?
+1 786-555-0142
via 305-555-0111
β†’ Confirmed for 2pm Friday
← BackBig Lou ● Unreadβœ“ Resolve
Hi, can you come by tomorrow?
10:42am
Yes β€” what time works for you?
10:43am
πŸ“Ž πŸ“‹
Send

The conversation list

Threads are grouped by (our CTM number, customer phone). Each row shows:

  • Linked lead's name (if matched) β€” bolded blue
  • Customer phone number (only when no lead match)
  • Our line: the CTM number + label + office
  • Last message snippet with direction arrow (← inbound / β†’ outbound)
  • Unread badge (count of unresolved inbound messages)
  • Relative time of last message

Filters

  • Search β€” fuzzy match across message body, lead name, phone digits
  • 🏒 Offices β€” multi-select dropdown to scope to one or more offices
  • Unread only β€” toggle to hide resolved threads
  • Clear all β€” appears once any filter is active

Inside a thread

  • Reply β€” textarea at the bottom. Enter sends, Shift+Enter adds a new line. 160-char counter for SMS-segment awareness.
  • πŸ“Ž Attach β€” image / video / audio / PDF up to 4MB. Sent as an MMS through CTM.
  • πŸ“‹ Templates β€” quick-reply popover with pre-saved text snippets (managed in CP β†’ SMS Templates).
  • ● Unread β€” flips the thread back to unread (useful when you read but want to come back to it).
  • βœ“ Resolve β€” clears the unread badge for the thread. New inbound messages reopen it automatically.

Reply-disabled forwarding numbers

Some CTM numbers are configured "no-reply" by an admin (CP β†’ CTM Numbers β†’ toggle). Threads on those numbers show the inbound history but the reply form is replaced with a notice ("This forwarding number doesn't accept replies").

Toll-free outbound SMS is blocked

Numbers with toll-free area codes (800 / 833 / 844 / 855 / 866 / 877 / 888) require a separate TCR / 10DLC carrier registration before they can send SMS. Until that's set up, the reply form on those threads is replaced with a πŸ“΅ notice: "Toll-free number β€” SMS not available. Use a different number to text this client back, or call them." Server-side /api/sms/reply also returns 403 for any attempt to send from a toll-free line.

Live sync

The SMS panel polls /api/last-change every second. When the server timestamp moves it triggers a fresh load of the conversation list AND, if a thread is open, the thread itself β€” so inbound messages and outbound sends from other agents appear within ~1 second. The πŸ“± button's unread badge updates in the same tick.

🚫 STOP / opt-out

TCPA compliance β€” when a customer texts STOP / UNSUBSCRIBE / CANCEL / END / QUIT / OPTOUT, we add their phone to a suppression list and refuse future automation SMS to them.

How detection works

The first whole word of every inbound SMS is checked. So:

  • "STOP" β†’ opt-out
  • "STOP texting me" β†’ opt-out (first word matches)
  • "stopwatch?" β†’ ignored (not a whole-word match)
  • "please stop" β†’ ignored (STOP isn't first)

What happens on opt-out

  1. The phone (last 10 digits) is added to sms_suppressions.
  2. The matched lead (if any) gets a timeline marker: 🚫 SMS opt-out β€” recipient replied "STOP".
  3. Any future automation that tries to SMS this number is refused at send time and logged as failed in the automation log.
  4. Manual SMS replies from the panel are NOT blocked β€” they're considered intentional 1:1 outreach.

Re-opt-in

Customer texts START / UNSTOP / YES / SUBSCRIBE / OPTIN / OPT-IN β€” phone is removed from the suppression list and the lead's timeline gets βœ… SMS opt-in.

πŸ“‹ Tasks panel

Slide-in to-do list. Same position + behavior as the SMS panel. Pulses red with a count when you have due/overdue tasks.

Live UI Preview Β· Tasks panel
πŸ“‹ Tasks + New
My Tasks
My Office
All
Call Big Lou before 5pm
⚠ Today 2pm πŸ‘€ Omri πŸ“₯ Big Lou
Submit Friday hours to dispatch
πŸ•’ Today 5pm 🏒 Sliding

Three scopes

  • My Tasks β€” tasks assigned to you specifically OR to one of your offices. Default tab.
  • My Office β€” only tasks assigned to your offices (not your specific user).
  • All β€” admin/owner only. Every task in the system.

Creating a task

Click + New. The editor modal asks for:

  • Title (required) β€” what needs to be done
  • Description β€” optional details
  • Due Date & Time β€” datetime-local picker
  • Assign To β€” radio: User (specific person) OR Office (anyone in that office; first to mark Done wins)

Linking to a lead or job

Open any lead β€” there's a πŸ“‹ button in the detail panel header next to Archive. Click β†’ task editor opens pre-filled with the lead link. Same flow on jobs (πŸ“‹ + Task button next to Short / Full / Rebook).

The created task has a clickable lead/job pill in the panel; the lead/job's timeline gets πŸ“‹ Task created: "..." on creation and βœ… Task completed: "..." on done.

Permissions

  • Create β€” any non-viewer.
  • Edit / delete β€” creator + assignee + admins.
  • Mark done / re-open β€” anyone non-viewer (any teammate can finish a shared task).

Cascade-delete on parent gone

If a linked lead or job is deleted, the task auto-deletes too β€” the work no longer applies.

πŸ” Recurring tasks

Owner-configured templates that auto-generate fresh tasks daily / weekly / monthly. Owner CP β†’ General Settings β†’ πŸ” Recurring Tasks.

Template fields

  • Title + description
  • Cadence β€” Daily / Weekly (pick day of week) / Monthly (pick day of month)
  • Time of day β€” 15-min steps
  • Assignee β€” User OR Office
  • Active β€” pause without deleting

How generation works

A cron tick runs every 15 minutes. For each active template:

  1. Computes "now" in EST so day-of-week / day-of-month math matches the dispatcher's calendar
  2. Decides if today is a firing day (always for daily; matches dow for weekly; matches dom for monthly)
  3. Skips if the configured time-of-day hasn't passed yet
  4. Idempotency-stamps via (template_id, occurrence_key) β€” never double-fires (occurrence_key is e.g. 2026-05-01 for daily, 2026-W18 for weekly, 2026-05 for monthly)
  5. Inserts a fresh task row with due_at = today's date at the configured time

Generated tasks are normal tasks β€” assignees can mark Done as usual. Already-generated instances stay untouched if you delete the template later.

🌐 Online Booking β€” overview & flow

Embeddable widgets that drop on customer websites. Submissions land as Leads with source Online Booking Tool, tagged to the office configured for that embed.

Live UI Preview Β· Customer-facing booking page (full-page mode)
BOOK ONLINE
Get a free quote
ACME Garage Doors
Schedule in 30s
βœ“
Service
2
Date & Time
3
Your Info
When works for you?
Pick a date, then a 2-hour window.
FRI
1
SAT
2
SUN
3
MON
4
TUE
5
WED
6
THU
7
9am–11am
11am–1pm
1pm–3pm
3pm–5pm
What to expect:
πŸ“ž
We'll follow up
Verification call.
πŸ’¬
The floor is yours
Share details.

The 3-step customer flow

  1. Service. Pick from the cards (or skipped entirely if 0 services configured). Each card can have a name, optional price, optional image.
  2. Date & Time. 7-day pill strip with previous/next-week arrows; past days greyed out. Pick a day β†’ 2-hour windows from open to close-2 appear.
  3. Your Info. First name (required) Β· Last name Β· Phone (required, US-validated) Β· Email Β· Address (Google Places autocomplete). A hidden honeypot field protects against bots.

What happens on submit

  • Lead inserted with source=online_booking and the embed's office
  • Notes carry the requested window + UTM params (if the host page passed any) + referrer
  • Existing lead-creation hooks fire: new_lead_webhook_url, lead_created automations, etc.
  • Page either shows the configured thank-you message OR redirects to the configured URL (mutually exclusive)

Test mode

Append ?test=1 to any embed URL to click through the full flow without inserting a real lead. The submit step short-circuits and the thank-you page renders anyway. Useful for the customer to verify the embed before going live.

πŸ›  Embed builder

Owner CP β†’ Automations & Integrations β†’ 🌐 Online Booking Tool. Each office gets one (or more) embeds.

Required fields

  • Internal Name β€” what the embed is called in the CP (not shown to customers).
  • URL Slug β€” the path piece, e.g. sliding β†’ https://gdq-leads.onrender.com/book/sliding. Must be unique.
  • Office β€” every submission is tagged this office.
  • Open Hour + Close Hour β€” drives the time slots. Need at least 2 hours between them.

Branding

  • Page Title β€” overrides the default "BOOK ONLINE" header (full-page mode only).
  • Company Name β€” shown in the form header.
  • Brand Color β€” color picker. Drives the primary button + step indicator + accent colors.
  • Logo β€” file upload (PNG / JPG / SVG up to 200 KB). Stored as a base64 data URL.

Services

Up to 3 rows. Each: name + optional price + optional image upload. Leave all blank to skip the Service step entirely (the customer goes straight to Date & Time β€” fastest possible flow).

"What to expect" sidebar

Up to 3 callouts with icon + title + body. Shown next to the form on desktop only (collapses below on mobile and is hidden in inline mode).

Confirmation mode

Radio choice β€” mutually exclusive:

  • Show a thank-you message β€” multi-line text, displayed on a confirmation card.
  • Redirect to a thank-you page β€” full URL. The page navigates the parent window (works through iframes too).

Domain whitelist

Comma-separated list of host domains that may load this embed. Empty = any domain. Useful so a competitor can't iframe your widget.

Other actions per row

  • βœ“ Live / β—‹ Paused toggle
  • πŸ“‹ Embed code β€” opens a panel with full-page URL + inline iframe snippet, both with copy-to-clipboard
  • β†— Preview β€” opens ?test=1 in a new tab
  • ⎘ Duplicate β€” clones the row (paused, suffixed "(copy)") so you can tweak a variant fast
  • Edit / Delete

πŸ“‹ Embedding on a website

Click πŸ“‹ Embed code on any embed row. Two formats are offered.

Full-page link

The customer navigates to your booking URL directly. Use it as a button on their site:

<a href="https://gdq-leads.onrender.com/book/sliding" target="_blank">Book an appointment</a>

Inline iframe

Drops the form right onto the customer's page. Adjust height if the bottom gets cut off:

<iframe src="https://gdq-leads.onrender.com/book/sliding?inline=1"
  style="width:100%;max-width:540px;height:720px;border:none"
  title="Book an appointment"></iframe>

UTM passthrough

If the host page links to the embed with UTMs (?utm_source=fb&utm_campaign=spring), they ride through to the lead's notes for attribution. The embed also captures document.referrer.

πŸ€– How automations work

Owner CP β†’ Automations & Integrations β†’ πŸ€– Automations. Rules that fire SMS or email when a job/lead event happens.

Anatomy of an automation

  • Trigger β€” what event fires it (job created, status changed, time before appointment, etc.)
  • Conditions β€” optional filters (office in [...], status in [...], job type in [...])
  • Recipient β€” who receives the message (assigned contractor / client / static address)
  • Channel β€” SMS or Email
  • Template β€” body (and subject for email) with {{variables}}

Sender identity

  • SMS β€” uses the office's CTM number (configured in CP β†’ Office Settings).
  • Email β€” uses the office's SMTP credentials.

Without those configured, automations for that office fail at send time and log failed in the log.

Quick example

"When a job is created in Garage office, SMS the assigned contractor with the appointment details":

  • Trigger = Job Created
  • Condition: Office = Garage
  • Channel = SMS Β· Recipient = Assigned Contractor
  • Body = Hi {{contractor.name}}, new job {{job.displayId}} on {{job.dateStart}} at {{job.timeStart}} β€” {{job.address}}. Client: {{client.fullName}} {{client.phone}}.

⚑ Trigger types

Three groups: job events, lead events, time-based.

Job triggers

  • Job Created (Submitted) β€” fires once on creation
  • Job Status Changed β€” pick the target status (Submitted / In Progress / Deposit / Pending / Done / Cancelled)
  • Job Rebooked
  • Contractor Assigned β€” when a contractor is set on a job that didn't have one OR the contractor is changed (Find Contractor / K-confirm reassign / Rebook with new contractor)

Lead triggers

  • Lead Created (any source) β€” fires for manual creates, bulk imports, AND every inbound source webhook
  • Lead Status Changed β€” pick target (Fresh / Not Yet Scheduled / Needs a Follow Up / Converted / Irrelevant)

Lead triggers cannot use the "Assigned Contractor" recipient β€” leads don't have one. The editor disables that option automatically when a lead trigger is picked.

Time-based triggers

Cron tick every 5 minutes:

  • X time before Job appointment β€” e.g. 24 hours before date_start + time_start
  • X time after Job end window β€” e.g. 2 hours after date_end + time_end
  • X time after Lead created β€” e.g. 1 hour after tr

Each rule has an offset: a number + unit (minutes / hours / days). Idempotency-stamped so the same (rule, job/lead) pair never fires twice.

🎚 Conditions

Every condition is multi-value (OR within a row, AND across rows). Empty = no constraint.

  • Office β€” filters on the row's office
  • Job Type β€” job triggers only
  • Job Source / Lead Source β€” label adapts to the trigger context
  • Current Status β€” filters on the row's status at evaluation time. Distinct from trigger_status (which is the status BEING ENTERED for *_status triggers). Lets you say e.g. "24h before start, only if still Submitted."

Example: "Garage repairs only"

Office = Garage Β· Job Type = Repair. Two rows AND'd together β€” fires only when both match.

Example: "Any service except installs"

Currently no NOT operator. Workaround: list every type EXCEPT install in the Job Type row. Future v3 work.

✏️ Templates & variables

Body (and email subject) support {{variable}} substitution. Empty/missing variables render as empty string.

Available variables

Job (job triggers only):

{{job.id}} {{job.displayId}} {{job.address}} {{job.city}} {{job.state}} {{job.postalCode}} {{job.dateStart}} {{job.timeStart}} {{job.dateEnd}} {{job.timeEnd}} {{job.jobType}} {{job.jobSource}} {{job.company}} {{job.status}} {{job.notes}}

Lead (lead triggers only):

{{lead.id}} {{lead.fullName}} {{lead.firstName}} {{lead.lastName}} {{lead.phone}} {{lead.email}} {{lead.zip}} {{lead.serviceType}} {{lead.callTiming}} {{lead.urgency}} {{lead.source}} {{lead.status}} {{lead.notes}} {{lead.campaign}} {{lead.adName}}

Client (works on both β€” points to whoever the job/lead is for):

{{client.firstName}} {{client.lastName}} {{client.fullName}} {{client.phone}} {{client.email}} {{client.address}}

Contractor (job triggers only): {{contractor.name}} {{contractor.phone}} {{contractor.email}}
Office: {{office.name}}

Recipient kinds

  • Assigned Contractor β€” uses the job's contractor (job triggers only). Skipped if no contractor.
  • Client β€” uses the customer's phone (SMS) or email (Email).
  • Static address β€” explicit phone or email entered into the rule. Useful for "always SMS our oncall at +1305..."

πŸŒ™ Quiet hours & pause-all

Two safety nets to stop automations from misbehaving.

Per-rule quiet hours

Each rule has a "Respect quiet hours" checkbox. When on, the rule only fires between 9am–9pm in the recipient's local timezone (derived from the job's postal code or the lead's stored timezone).

  • Event-driven rules outside quiet hours log skipped: outside quiet hours and never fire.
  • Time-based rules outside quiet hours skip the current 5-min tick WITHOUT stamping idempotency, so they retry once business hours open.
  • The list view shows a πŸŒ™ QH badge on rules with this setting.

Global pause-all

Big "Pause All" button at the top of the Automations panel. Click β†’ confirmation β†’ every fire path short-circuits. A red banner appears: "⏸ Automations are PAUSED β€” no rules will fire until you resume." Click β–Ά Resume All to flip back. Use it as a kill-switch when something's wrong.

πŸ“œ Log & troubleshooting

Click πŸ“œ View Log at the top of the Automations panel. Last 200 events.

Each row shows

  • Status icon: βœ“ sent Β· βœ— failed Β· β—‹ skipped
  • Rule name + trigger kind + channel
  • Recipient phone or email
  • Linked job (display_id) or lead (name)
  • Error message (red, when failed)
  • Rendered preview (the actual text/email sent)
  • Timestamp

Filters

Top of the modal: search box (matches recipient / preview / error), status dropdown, channel dropdown, per-rule dropdown, Reset button. Search is debounced 250ms.

Common failures

  • "No CTM number configured for office X" β€” set one in CP β†’ Office Settings.
  • "SMTP not fully configured for office X" β€” fill in From email + host + port + app password.
  • "Recipient ... has opted out of SMS" β€” they replied STOP. Manually call/email them instead.
  • "contractor has no phone" β€” the rule targets the assigned contractor but their phone field is empty in the Contractors page.

βš™οΈ Office Settings

Owner CP β†’ Automations & Integrations β†’ βš™οΈ Office Settings. Per-office configuration: CTM number for SMS, SMTP credentials for email, and the Jobs feature toggle.

πŸ”§ Jobs Feature toggle

Each office card has an "Enabled" checkbox at the top. When unchecked:

  • Users assigned only to that office can't open /jobs, /contractors, or /service-areas β€” they redirect to /leads.
  • Top-nav links to those pages hide for them.
  • The Reports modal hides the Jobs tab for them.
  • The office's existing jobs are excluded from /api/jobs queries β€” the dashboard treats them as if they didn't exist (rows preserved in the DB; flipping back on restores everything).
  • The office's converted leads keep the legacy "πŸ”§ Mark as Opened in Workiz" flow.

The header line of each card shows the live status: "πŸ”§ Jobs enabled" / "πŸ”§ Jobs disabled."

CTM number

Each office picks ONE CTM number from the synced list. Used as the from for any SMS automation scoped to that office. The dropdown is searchable β€” type to filter by phone, name, or queue.

SMTP credentials

  • From Email β€” the SMTP login email (also used as the From address by default)
  • From Name β€” display name shown to recipients
  • SMTP Host β€” e.g. smtp.gmail.com
  • Port β€” 587 (STARTTLS) or 465 (SSL)
  • App Password β€” Gmail and Outlook block plain passwords; you must generate an "app password" via account β†’ 2FA β†’ app passwords. Custom-domain SMTP works with the regular mailbox password.

Passwords are encrypted at rest with AES-256-GCM. The browser never sees the stored value back; an "βœ“ already set β€” leave blank to keep" hint replaces the field on edit.

πŸ“¨ Send Test

Once configured, the Send Test button prompts for a recipient and pings them through the saved creds. Verifies host/port/auth before relying on the rule for real sends.

πŸ• Calling hours

Owner CP β†’ General Settings β†’ πŸ• Calling Hours. Per-state windows defining when CSRs are allowed to call.

What it controls

  • Lead detail panel β€” shows a "outside calling hours" warning when the lead's local time is outside the window for their state.
  • Quiet-hours fallback β€” automations with "Respect quiet hours" use 9am–9pm local time as the default, but a future enhancement could honor per-state hours from this table.

Each state has start/end hours (24h, integer) plus optional Sunday hours.

πŸ“₯ Lead sources

Owner CP β†’ General Settings β†’ πŸ“₯ Lead Sources. The list of channels leads can come from.

Each source has:

  • Key β€” internal slug (e.g. facebook, yelp). Match against incoming webhook payloads.
  • Label β€” display name (e.g. "Facebook Ads"). Shown in dropdowns and pills.
  • Color β€” pill color for the dashboard.
  • Active β€” toggle to hide a source from new submissions without deleting it.
  • Webhook token β€” auto-generated. Append to /webhook/s/<token> for source-specific ingestion (forces the source key, ignores body's source field).

Built-in sources

facebook Β· google Β· lsa Β· yelp Β· angi Β· thumbtack Β· website Β· manual Β· nextdoor Β· bookback Β· online_booking. Add more from the CP β€” same flow.

πŸ”— Outbound webhooks

Owner CP β†’ Automations & Integrations β†’ πŸ”— Webhooks. Five separate URL slots so different events route to different downstream automations.

SlotFires when
Jobs Webhook URL (Workiz format)Job created (Submitted) AND job β†’ Done
Deposit Webhook URLJob β†’ Deposit (only)
Cancelled Jobs Webhook URLJob β†’ Cancelled
New Lead Webhook URLAny new lead β€” manual / bulk / inbound webhook / online booking
Cancelled Leads Webhook URLLead β†’ Irrelevant

Plus a separate BookBack Jobs Webhook URL (CP β†’ BookBack AI) that fires only on Submitted (not Done). All payloads are Workiz-shape so a single Zap can consume any of them.

Payload includes

UUID, SerialId, JobDateTime, JobEndDateTime, CreatedDate, Status, Phone, Email, FirstName, LastName, Company, Address, City, State, PostalCode, Country, Unit, Latitude, Longitude, JobType, JobSource, ServiceArea, Tags, Team (the contractor's NAME only β€” never private contact info), AreaCode, OfficeAddress, DepositAmount, DisplayId. Plus a few lead-only extras when fired from the leads side (LeadId, Office, Urgency, Campaign, FbLeadId, IrrelevantReason).

Fire-and-forget

Webhook calls don't block the API response. Failures log to the server console but don't bubble to the user. If a downstream URL is offline, the event is lost β€” there's no retry queue (yet).

πŸ“₯ Inbound lead webhooks

Several endpoints accept inbound leads. Each has slightly different auth.

The endpoints

  • POST /webhook/leads β€” generic. Requires x-webhook-secret header (or ?secret=). Source comes from the body's source field, fallback to ?source=, default manual.
  • POST /webhook/facebook β€” legacy alias. Same auth as above; source forced to facebook.
  • POST /webhook/s/:token β€” source-token-protected. Token comes from the source row in the CP. The source key for the lead is forced from the token (body's source field is ignored).

Body fields recognized

The handler accepts many common shapes:

  • full_name, fullName, or name
  • email
  • phone_number or phone
  • zip_code or zip
  • lead_id or leadId (stored as fb_lead_id; not used for dedup)
  • ad_name / adName
  • adset_name / campaign / campaign_name
  • partner_name
  • office (defaults to General)
  • what_can_we_do_for_you_today / service_type
  • how_soon_do_you_need_the_service / how_soon
  • created_time (defaults to now)
  • extra_notes / message / notes

Rate limit

100 leads per source per 10-minute window. Exceeding returns 429 to the source. Tracked in memory per server process.

BookBack reactivation

When source = bookback, the handler matches the incoming phone against existing leads. On match: reactivates the existing lead (resets to Fresh, appends notes, records a bookback_calls row) instead of creating a new one. See the BookBack chapter.

πŸ“Š Reports

Topbar β†’ πŸ“Š Reports. Wide modal with two tabs (πŸ“₯ Leads Β· πŸ”§ Jobs) and four period buttons (Today / This Week / This Month / Custom) plus an office multi-select. The Jobs tab is hidden when the user has no jobs-enabled office.

Leads tab β€” what's measured

  • KPI tiles: Total Leads Β· Converted Β· Irrelevant Β· Conversion Rate Β· Fresh Β· Not Yet Scheduled Β· Needs Follow Up Β· Avg Response Time
  • Breakdown bars: Leads by Status Β· by Source Β· by Office
  • Lead status by day β€” stacked daily chart (Converted / Open / Irrelevant)
  • BookBack AI β€” daily performance β€” Detected / Reactivated / Converted bars
  • Follow-up activity β€” call attempts vs same-day conversions on past leads
  • Conversion efficiency β€” total leads vs converted, broken down by attempt # (1st call, 2nd call, …)
  • Agent performance table: Calls Β· Claims Β· Conversions Β· Avg Response Time

Jobs tab β€” what's measured

  • KPI tiles: Total Jobs Β· Completed (with done %) Β· Cancelled (with cancel %) Β· Revenue (sum of closing_total on Done) Β· Deposits Β· Rebooked Β· K-Accepted Β· Parts Cost
  • Breakdown bars: Jobs by Status Β· by Office Β· by Type Β· by Source
  • Daily jobs created β€” stacked daily chart (Done / Open / Cancelled)
  • Top contractors β€” table with Jobs Β· Done Β· Cancelled Β· Revenue per contractor

Auto-emailed reports

Owner CP β†’ General Settings has the Email Settings panel. Configure recipients + tick which periods (daily / weekly / monthly) should auto-send. Schedule (EST):

  • Daily β€” every night at 11 PM EST
  • Weekly β€” every Sunday at 11 PM EST
  • Monthly β€” 1st of the month at 11 PM EST

Email content mirrors the on-screen report β€” KPIs, breakdown bars, inline SVG charts, agent performance, plus the full Jobs section. Date windows are EST-aligned (a previous bug was clipping daily reports to ~3 hours of data β€” fixed).

πŸ‘₯ Users & permissions

Topbar β†’ πŸ‘₯ Users. Visible to admins / owners / viewers; only admins+ can edit.

Per-user fields

  • Name + email
  • Role β€” viewer / agent / admin / owner
  • Office β€” comma-separated list (Sliding,Garage) or all for owner-equivalent visibility
  • Active β€” uncheck to disable login without deleting the user
  • Password β€” set a new one on edit (never displayed back)

What roles can do (recap)

See the Login & roles chapter for the full matrix. Tl;dr: owner does everything (including the CP), admin runs the day-to-day write actions, agent works leads + reads jobs/contractors/areas, viewer reads everything. Page visibility is open to every role; write capability is gated by API role checks.

πŸ“‹ Audit log

Topbar β†’ πŸ“‹ Audit Log. Owner-only. Every system event recorded chronologically.

What's recorded

  • New lead inserts (per source)
  • Lead status changes
  • Job creates / status changes / rebooks
  • BookBack reactivations
  • Webhook rate-limit hits
  • Office adds / deletes
  • Calling-hours updates
  • Lead archive / restore / delete

Each row carries the actor (user name or "System"), the office (or "all"), a short text description, and a JSON metadata blob with structured details. Filter by type and date range.

⏱ Global timeline

Topbar β†’ πŸ“‹ Timeline. Admin / viewer / owner. The fast-scrolling activity feed across every lead.

Each entry: timestamp + lead name + entry type (status / call / note / sys) + body. Use it to "what's been happening" at a glance without opening individual leads. Scroll back as far as you like β€” paginated infinitely.

Filters

  • Office (multi-select)
  • Type (Status changes / Calls / Notes / System)
  • Date range (preset or custom)
  • User (who performed the action)

βš™οΈ Other CP settings

A grab-bag of smaller owner-only settings. Reach them from CP landing β†’ the relevant tile.

Job Settings

  • πŸ›  Job Types β€” values for the New Job + Contractor dropdowns. Each has a label + sort order.
  • πŸ“Š Job Sources β€” same shape, used for the Job Source dropdown.
  • β›” Cancel Reasons β€” pre-canned reasons for Cancelled jobs.
  • 🏷 Job Tags β€” color-coded tag definitions used in the job detail panel.

Numbers

  • πŸ“ž CTM Numbers β€” synced from CTM via API. Each row can be assigned an Office and toggled "no-reply" (used by SMS panel to disable replies on certain numbers).
  • πŸ”΅ LSA Accounts β€” Local Services Ads account directory (LSA customer_id β†’ office mapping).
  • πŸ’¬ SMS Templates β€” quick-reply text templates surfaced in the SMS panel's πŸ“‹ button.

Automations & Integrations

  • πŸ“² Office Settings β€” covered above.
  • πŸ€– Automations β€” covered above.
  • πŸ”— Webhooks β€” covered above.
  • ⚑ BookBack AI β€” single URL field for the BookBack Jobs Webhook (fires on Submitted only).
  • 🌐 Online Booking Tool β€” covered above.
  • 🌐 Google Places API β€” single field for the Google Places API key. Used by the address autocomplete on New Job + the bulk Service Areas geocoder + the in-app Google Maps panels. Restrict the key by HTTP referrer in Google Cloud Console.

App Settings

  • Escalation Timer (minutes) β€” how long a Fresh lead can sit unclaimed before it counts as escalated in reports.

General Settings

  • πŸ“₯ Lead Sources β€” covered above.
  • ⏱ Snooze Sequences β€” covered above.
  • πŸ• Calling Hours β€” covered above.
  • 🏒 Offices β€” the master list. Add / remove offices. Deleting one is blocked if any active lead/user/CTM number references it (force-delete via prompt confirms).
  • πŸ” Recurring Tasks β€” covered above.