Sales Analytics answers a manager's four questions: Are we on pace? Which rep needs me? Which lead is about to be lost? What did the AI do vs my people?
It is a coaching instrument, not a bookkeeping report. The page evolves the shipped Analytics tab (now Reports › Sales) — a four-tile clickable hero (Sold · Unresponded · Untouched · Total Touchpoints) at the top, the Team Accountability table as the centerpiece, the Unresponded queue for customers waiting on a reply, a Reports menu in the page header carrying the seeded standards plus whatever we authored for that store, and an honest human/AI split running through every number.
| Date | Ruling |
|---|---|
| 09-15 | Sales Analytics moves to Reports › Sales (navigation redesign). |
| 07-10 | AI = one combined bucket — automation-flagged sends UNION the DAISI actor; one "AI" label, never blended into human rows or totals. |
| 07-10 | Black hole is strict + immediate — no open task AND no future appointment; an automation enrollment is NOT follow-up; no grace period. |
| 07-10 | Targets = New / Used only — Fleet excluded; targets come from dealership Settings; no targets → the empty state, never fake numbers. |
| 07-10 | Desk-by-lane bars kept — WITH the mandatory coaching empty state (never looks broken). |
| 07-10 | No leaderboards — threshold callouts only; values get color, never rows, never ranks. |
| 07-10 | Behaviors grouped before outcomes — the column-group band order is fixed. |
| 07-10 | Accountability table sits ABOVE the cards (ruling 7). |
| 07-10 | DAISI row pinned bottom — ruled off from humans, muted identity, honest numbers. |
| 07-10 | "Last Active" naming — new metric from dealerjourney MAX(datetime) per dealer; presence dot = activity ≤15m, not online status. |
| 07-10 | Evolution law — build on top of today's Analytics page; no restyling of the shipped gospel modules. |
| 07-29 | No Follow-Up RETIRED, replaced by Unresponded (inbound with no reply since) — tasks-and-appointments never described what a rep actually owes a waiting customer. |
| 07-29 | Inbound calls count MISSED only. Answered calls leave no reply row and were roughly half the flagged set at one store as pure artifact. |
| 07-29 | The AI cut reuses the existing AI Included/Excluded toggle rather than a new control — which makes the excludeAi dashboard parameter a hard prerequisite (design-only today). |
| 07-29 | Automation and drip sends never count as a reply; a queued scheduled message does not, a fired one does. |
| 07-29 | Opted-out/STOP repliers and Closed/Bad status are excluded from Unresponded. |
| 07-29 | Computed from the customer timeline, never from the respondedAt stamp. |
| 07-29 | The Unresponded tile is always range-bounded. |
| 08-03 | Reports live in the page header, not inside a data card — navigation never lives in a data card; a card's controls act on that card. |
| 08-03 | Header order is scope, then AI, then destination — Team/Mine | AI | Reports, with the date range and Filters on the right. |
| 08-03 | Dealers get no report builder. Definitions are authored in the launch portal and carry a scope of either the whole tenant or specific users; the only dealer-facing knob is the date range. |
| 08-03 | The four Activity reports are seeded standards in the Reports menu. |
| 07-30 | Two roles, one table. BDC and Sales are stacked sections inside the single accountability card — same range, filters and sort. Not two cards, not a role filter: the comparison across the floor is the point. |
| 08-13 | The hero is two rows of four — the throughput strip, then the keeping-up row. Avg Response joined the second row to balance it after Sold moved to the strip; it is a median, colored by the calibrated bands (neutral under 15m · orange 15–60m · red over an hour) on the value only, and it drills into the Team Accountability table. |
| 08-12 | The throughput strip is throughput, not stock — four period counts (Engaged · Visited · Desked · Sold); one deal can appear in all four, so they never divide into rates and carry no connectors ever. |
| 08-12 | Sold reads closeDate and reconciles with the DMS — never the closed-lane timestamp, which ran ~35% low at one store (85 DMS-confirmed sales never dragged to the lane). |
| 08-12 | Desked reads lane-entry-in-range regardless of outcome — a desked deal that sold or died still counts; the raw lane count accumulates forever (730 all-time vs 601 in 30 days). |
| 08-12 | Engaged is the raw inbound count — distinct customers with an inbound text, email or call, no deal filter, because “customers who reached out” needs no asterisk. |
| 08-12 | There is no Delivered tile — no trustworthy delivery signal exists (13 completions all-time at a busy store); a tile reading 1 because nobody logs it is a lie dressed as a metric. |
| 08-04 | BDC outcome columns are Appts Created and Appts Shown. Appts Created counts the appointments that actor created (attribution: the creator; anchored on the created date). Appts Shown counts appointments scheduled in the range and marked complete, credited to the customer's assigned dealer (anchored on the scheduled date) — so it is shared with the sales band, identical definition, not role-specific. The two are independently attributed and never form a ratio or funnel; marked-complete means Appts Shown undercounts, so no show rate. |
| 07-30 | Shared columns, role-specific columns. Calls, Texts, Emails, Tasks and Avg Response appear for both roles; Appts Shown is shared by both bands with one definition, while the other outcome differs — Appts Created for BDC, Sold for Sales — each section showing its own set rather than padding both with blanks. |
| 07-30 | Managers and role-less users are excluded from the accountability table. It measures the people working leads. |
| 07-30 | A role is a marker on the user, not their job title. Job title is free text and display only; the structured role marker drives the split. |
| 07-30 | Unresponded — nobody replied, human or AI. A reply from the AI clears it exactly as a human reply does; the metric measures neglect, not staffing model. |
| 07-30 | A blast is not a reply. Automation and drip sends never clear Unresponded. Neither does a queued scheduled message — only one that actually went out. |
| 07-30 | Answered calls are out, missed calls are in. An answered inbound call leaves no reply record, so it cannot be judged; a missed one is the clearest possible unanswered signal. |
| 07-30 | Compliance is not neglect. Opted-out customers and STOP replies never appear in Unresponded. Closed and Bad statuses are excluded too. |
| 07-30 | Time-bounded, always. Unresponded follows the page's date range. Unbounded it is an eleven-year graveyard: 242,000 customers platform-wide, 99.6% of them with nothing unanswered inside three days. |
| 07-30 | The AI toggle is the human-diligence view. With AI included, a DAISI answer counts as handled. Flipping to AI excluded surfaces the customers only the AI ever answered — that is the intended way to ask “are my people engaging?”, and it is why no second tile is needed. |
| 07-30 | One definition, three surfaces. The Unresponded tile, the Contacts filter it opens, and the Unresponded filter never diverge. |
| 07-11 | Hero = four clickable metrics — Sold / Unresponded / Untouched / Total Touchpoints; the Desk-by-lane bar removed (duplicated the Lead Funnel card); pace ships target-free, the goal does not. |
| 07-13 | Header rework — the Analytics title row is dropped; the header is the shared root-header grammar (quick-filter chips + date chip + Filters button); the location dropdown is retired to the Filters panel; panel filter set = Included Users + Locations (ship today) with Lead Source / Traffic Source / Inventory as prescriptive additions. |
| 07-13 | Filter panel rebuilt to native FilterPageForm grammar (extracted from the Contacts/dashboard filter source): date field + 2-col shortcut grid, Included Users assignment selector w/ user chips, Lead/Traffic Source as chip-trigger fields w/ search overlays, Inventory as multiselect chips; header date chip opens the Filters panel (caret removed). |
| 07-13 | Filter panel production tightening — panel chrome (Apply Filters header action + gear + Clear All/Apply footer), dual-month custom-range popover, chip-trigger selected state = blue outline + blue-02 fill + trailing pencil edit (reusable law); panel now opens from the date chip and Filters button in the screen mocks. |
| 07-13 | Atomic audit — multiselect chips = gray-06 fill unselected / blue-02 + x selected (atom-level, cascades); filter section headers = gray-07 band + gray-06 underline w/ container-title style; panel pushes content (split-view), never covers; all-caps buttons RETIRED DS-wide (Title Case); page body = systemWhite token. |
| 07-13 | Atomic sweep — info glyph fa-regular DS-wide + info seats next to its label (removed .sb-stat far-right margin + section-title flex spacers); mobile page body = systemWhite + canonical .sb-mh-tabs band; mobile Team Accountability now the horizontal-scroll .sb-acct table (card stack + .sb-acct-card atom retired from the screen); dead rules (grpband, .sb-stat .k) and local style forks removed; .val.alert, .fbar/.vals tokens, merged .sb-locgrid/.sb-mschips. |
| 07-14 | Table styling — column-header row = gray-07, 48px, nowrap+ellipsis (never wraps); Reports controls row also gray-07 so the whole top block is gray-07 above the gray-06 "All" totals; accountability avatars s24→s20, rows 54px; Last Active is relative-only ("1d ago", not "Yesterday"). |
| 07-14 | Accountability avatars removed — rep rows are now index + name/role only (the s20/s24 avatars distorted into ovals and added no value); the Daisi row keeps its name + AI chip, no avatar. Dead .avwrap/.presence/.ai-av CSS retired. Compact/density toggle standardized on fa-grip-lines. |
| 07-14 | Feature Header Inventory — new comp-header-inventory page logs every feature's desktop + mobile header in one place (shared chrome factored out; FULL vs COMPACT mobile variants; desktop↔mobile parity). Defines the Tasks header (All/Mine · All/Overdue/Due-Today status filter · Compact · Filters · +New Task; no date-range — status is the date cut) and the Calendar header (All/Mine · Unresolved · Compact · Filters · +New Appointment; date navigated via the grid) ahead of building those screens. |
| 07-14 | Header controls refinement — new control-group divider atom .sb-mseldiv ([assignment] | [status] | [Compact] | [actions]); primary Add keeps its text label on mobile compact rows; Leads Add restored (desktop + mobile); overdue/unresolved status use the warning triangle (fa-triangle-exclamation); Tasks status is a two-state toggle (All ↔ Overdue), "Due Today" removed; inventory-page mobile frames de-contained (squared, no shadow). |
| 07-14 | Header Controls — States & Behavior — new comp-header-controls-states page: the canonical interaction-states + behavior spec for the .sb-msel header control family (base states · single-select group · two-state toggle · smart-view + count · density toggle · multi-select chips), with LIVE clickable demos. The Feature Header Inventory (composition) now links out to it (behavior) instead of duplicating state notes. |
| 07-14 | Shared sb-controls.js + data-role convention — header-control interactivity is now provided by one shared script (included like sb-tooltip.js) wired by a data-role convention (single|toggle|two-state; data-group for a single-select set); the states page is script-driven (no bespoke JS), and feature screens opt in by adding the attributes + the include. |
One table answering "is my team doing the work, and is it turning into sales" — its anatomy, thresholds, AI row, and contract.
Two roles, one table. The split uses the dealers roundRobinRoles marker — sales_person vs bdc_agent — excluding managers and role-less users. BDC and Sales are stacked sections inside the single accountability card: same date range, same filters, same sort. Not two cards, and not a role filter: the comparison across the floor is the point. Calls, Texts, Emails, Tasks and Avg Response mean the same thing for both roles and appear for both. Appts Shown is likewise shared, with one definition in both bands — appointments scheduled in the range and marked complete, credited to the customer’s assigned dealer. What differs per role is the other outcome column: Appts Created for BDC (attribution: the creator) and Sold for Sales, so each section shows its own set rather than padding both with blanks. A role is a structured marker on the user, not their job title; job title is free text and display only.
Honestly: only 5 of 107 dealerships have anyone marked bdc_agent today, and the field is gated behind auto-assignment — so it reads as never-configured rather than empty. The ruling is to ship the split anyway because the feature is rolling out.
Appointment attribution caveat. There is no creator column: attribution needs the notifications proxy at ~90% coverage, ~30% of “dealer” creates are actually DAISI, 31% of appointments are customer self-scheduled with no rep at all, and 39% sit unresolved via a silent hourly sweep — so do not ship a bare show-rate percentage.
How every number on the page separates human effort from the machine's.
Four big clickable counts at the top of the page — who reached out, who showed up, who got desked, who bought — each one a door into the list behind it.
The tile and its element spec are drawn. What's missing is the destination: the Unresponded view has never been instanced onto the assembled Contacts screens.
The tile that answers “who is waiting on us right now” — customers who reached out and have not heard back.
Definition. A customer who sent an inbound text, email, or missed call inside the selected date range and has had no reply for over 30 minutes. Clicking the tile opens Contacts, filtered to unresponded and sorted longest-wait-first. A dedicated Unresponded saved view is a separate feature and is parked. Tooltip copy: “Customers who reached out and haven't heard back. Counts inbound texts, emails and missed calls in this range with no reply since.”
Counts as inbound: sms/send, email/send, call/send-fail (missed). An answered inbound call is excluded — it leaves no reply row, so it would read as neglect purely as an artifact. Counts as a reply: any outbound human or DAISI message. Not a reply: automation and drip sends, and scheduled messages still queued (one that actually fired does count). Excluded: opted-out customers and STOP repliers (you must not reply to them), and customers at Closed or Bad status.
Measured from the timeline, not the respondedAt stamp. respondedAt is written by a single async, forward-only writer that clears a customer's entire historical backlog on one reply, fires on both direction branches for calls, and silently under-writes per tenant — one live dealership stamps it on 0.4% of inbound texts while others hit 78–96%. The timeline is immutable and self-healing.
Range-bounded by construction. Unbounded, this is 242k customers back to 2015; 99.6% have nothing unanswered inside three days. The tile always reads the selected range.
AI split. The tile obeys the existing AI Included / AI Excluded toggle. AI-included counts a DAISI reply as handled; AI-excluded surfaces customers only the AI touched — the human-diligence view.
Engineering reality, verified. The unresponded filter shipped 2026-08-05 on customers/search-pt1 with an identical predicate to the tile (30-minute grace, respondedAt null, automation-flagged inbound excluded, email/sms/call) — but it carries no date window, so until that window is passed through, the landed list over-counts the tile. That is the only parity gap. LeadsFilters has no unresponded field yet, so the app cannot send the filter. No wait-based sort key exists in the customer-search sort whitelist, so longest-wait-first is an engineering ask. The AI-Excluded / AI-Included distinction is not implementable for this metric: no stat in the sales-dashboard generator takes an AI parameter, and the only automation check sits on the customer's inbound message rather than on who replied — which is why the AI sentence left the tooltip.
Open questions. Should AI-Excluded be the default for this tile? AI-included flatters heavy-DAISI stores — a 13× spread between two live tenants. Is “Unresponded” the right word, or “Awaiting Reply”? Should a manually logged call count as a reply? Should archived customers be excluded?
The fixed reports a store can open from the page header — the control, its menu, the one view they all render through, and how a report comes into being.
The hero is two rows of four. The This-period strip on top (Engaged · Visited · Desked · Sold) counts what moved through the store in the period; the keeping-up row beneath it (Unresponded · Untouched · Avg Response · Total Touchpoints) says whether the team is staying on top of it. Every tile taps to its view — Avg Response drills into the Team Accountability table on this page, because it is a per-rep coaching number.
Straight from a GSM interview at Galleria BMW: the one thing that would make her open Analytics instead of her DMS reports is a live read of the floor — who is here right now, which rep they're with, and how long they've been waiting. So the strip sits just under the hero, above Team Accountability.
It is an instance of the Showroom atoms, not a second showroom — .sb-shwm-ctn, the .sb-svisit rows, the duration legend, the .sb-rts road-to-sale chips, .sb-avgroup reps, and the .sb-dealrow vehicle of interest all come from comp-showroom-screen, which owns the grammar. Time-in-store keeps the showroom banding: under 2h neutral, 2h–4h warning, over 4h red. The five road-to-sale chips (Greet, Test Drive, Appraisal, Manager T.O., Credit App) surface her killer coaching case at a glance — a deal that reached Manager T.O. with Test Drive still unchecked.
The strip is a glance that hands off, never a place to act: the whole row is a click target → the Showroom view and the strip footer carries the canonical View All text button (the SectionActionTextButton), but Analytics never mutates a visit — no check-out, no editing, no toggling road-to-sale here.
Pace only, no goal. Pace is this month's run rate projected to month end; it needs no target, and it reads low in the first week because DMS close dates arrive late. The Sold tile carries a New / Used split in place of a goal (New 81 · Used 61) that adapts or hides when one condition exceeds ~95% of a store's sales — a pure-used independent measures 100% used, a franchise ~57% new.
Why no goal: no targets model exists anywhere in the platform — not Postgres, not the DynamoDB dealership config, not the Flutter app, not the ops portal. A dealership-level scalar entered in the internal ops portal was rejected because a GSM still could not set their own number. Per-rep and per-location targets deserve their own epic.
The work band leads the page as its live layer: four queues — Unresponded, Untouched, Expected Today, Rescue — over a Today-so-far data row. That row anchors to now, never to the page date chip: Analytics and the Calendar are sibling tabs sharing one filter state, so a chip-reading row would silently degrade into “selected range”.
Queue definitions as shipped. Untouched keeps a 48-hour window and splits into nobody-touched plus an AI-only remainder — unwindowed, a never-contacted count measures in the thousands and is unusable. Unresponded is no reply for over 30 minutes, scoped to the selected date range, longest-wait-first. Rescue is 72 hours and only deals that were actively worked, excluding the automated cleanup sweep.
Lane aging. Each Desk lane carries a p50 days-in-lane and a count over 30 days — never a mean, because the tails are extreme (a real tenant has 1,430 deals in Working at a 103-day median with none under 30 days). The measure is days since last entry into the lane, and stamp coverage is tenant-variable.
Destinations. Every clickable element carries a data-dest attribute that dev mode surfaces as a “goes to” line, in three states — a real path, “needs: what is missing”, or “prescriptive: no screen yet”. Today that is 8 real, 5 needs, 1 prescriptive.
Tap a rep row and get everything a coaching conversation needs — one rep, one page.
The assembled desktop composition and its mobile counterpart — same data, same modules, two idioms.
How the shared .sb-table reads on the Analytics tables (accountability + Reports).
--compact stays 32px).Status coloring for the accountability value cells (values only get color, never whole rows). Grounded in the 14-day inbound-response distribution: median 6.1m, p75 21.7m, 61% respond within 15m, 18% take over 60m.
All Activity, Call Activity, Text Activity and Email Activity are four of the seeded standard reports every dealership gets at setup. They sit in the header Reports menu under “Standard reports”, alongside Team Performance. Each opens its activity list through the fixed-report template; Export CSV downloads the rows in the selected range.
Every number on this page carries a hover tooltip. This is the dictionary, and it exists because a metric a manager cannot define is a metric he will not trust.
| Metric | Tooltip (what the user reads) | What it actually measures |
|---|---|---|
| Sold | “Deals sold in this period. Matches the DMS record when one exists.” | closeDate in range, any source, DMS-matched preferred — never the closed-lane timestamp. Credited to the customer's assigned rep; a deal reopened and reclosed counts twice. |
| Engaged | “Customers who reached out — a text, email or call in — during this period.” | Distinct customers with an inbound SMS, email or call in range, timeline-derived; no deal filter. |
| Visited | “Sales appointments that actually happened in this period.” | Sales-type appointments in range with status completed or in_showroom, or a recorded visit start. Service and delivery appointments never count. |
| Desked | “Deals that hit the desk in this period — numbers were worked.” | Deals whose desking-lane entry falls in range, regardless of where the deal sits now. |
| Unresponded | “Customers who reached out during this period and still haven't heard back — from a person or from the AI. Click to open them in Contacts, longest wait first.” | See the Unresponded section. |
| Untouched | “New leads nobody has contacted yet.” | Leads created in the range with zero outbound messages since — email, text or call. |
| Outbound Touches | “Every outbound call, text and email your team sent.” | The sum of outbound calls, texts and emails in the range. Currently labelled Total Touchpoints, which collides with a report column that also counts notes. |
| Open Tasks | “Tasks still to do.” | Open to-do tasks touched inside the range. Currently labelled “Tasks Today”, which is neither today-scoped nor due-date-based. |
| Overdue | “Tasks past their due date.” | Open tasks whose due date has passed as of now — always as-of-today, even when looking at a historical range. |
| Calls | “Outbound call attempts, including ones that didn't connect.” | Outbound call events plus manually logged phone calls. |
| Texts / Emails | “Outbound texts (emails) sent by this person.” | Outbound message events by that rep. The communication chart on the same page counts a different set, including automation — the two numbers are not the same population. |
| Avg Response | “Median time to reply to a customer's text or email. Calls aren't counted.” | A median, not an average, over inbound texts and emails, credited to the customer's assigned rep rather than whoever actually replied. |
| Sold (accountability column) | “Deals closed in the selected period.” | Same as the hero Sold, per rep. Currently labelled “Sold Today” on the screens even though it follows the range. |
| Last Active | “When this person last used the CRM.” | The rep's most recent CRM activity. Goes silently blank for long-idle users because the underlying history is pruned. |
| Appts Created | “Appointments this person booked.” | Appointments that actor created — attribution is the creator, anchored on the created date. BDC band only. Not comparable to Appts Shown: different attribution, different anchor. |
| Appts Shown | “Appointments that actually happened for this person's customers.” | Appointments scheduled inside the range and marked complete, credited to the customer's assigned dealer rather than whoever booked it. Same definition in both bands. Because it requires marked-complete, the appointments left unresolved by the silent hourly sweep never count — it undercounts, so never derive a show rate from it. |
| Appointments: Scheduled | “Every appointment on the calendar for this period.” | All appointments whose date falls in the range, whatever became of them. Currently labelled “Set”, which collides with a report column meaning something narrower. |
| Appointments: Confirmed / Shown / Missed / Declined | “Appointments the customer confirmed / showed up for / missed / cancelled.” | The appointment's status. A large share of appointments are never resolved either way, so a bare show-rate percentage would be computed on an unreliable denominator. |
| Time in store | “How long this guest has been here.” | Elapsed time since check-in on an open showroom visit. |
Six relabels: Tasks Today → Open Tasks · Sold Today → Sold · Set → Scheduled · Avg Response split into First Response and Reply Time · Total Touchpoints → Outbound Touches · and the Lead Funnel stage names aligned to Desking and Financing.
What is an instance of existing grammar vs genuinely new — plus the defects to fix while in the code.
unresponded Contacts filter shipped 2026-08-05; LeadsFilters in the Flutter client has no field to send it. untouched shipped 2026-07-21 as a Desk-board Fresh-column filter (not a new screen); same client gap. The promised longest-wait-first sort exists on neither side and is genuinely new.excludeAi must become a real dashboard parameter end to end — today it exists only in the design.closeDate date target; the Close-lane destination under-counts sold by up to 15% because sold deals are not always dragged into the lane. The backend supports dateTarget=closeDate; the Flutter DealsFilters has no dateTarget field.createdAt, updatedAt and closeDate only.respondedAt has no index. Without a partial index this slows the ENTIRE Contacts list, not just the tile.excludeAi a hard prerequisite) is unimplementable on that column. Rewrite as a timeline anti-join: latest inbound per customer vs any dealer outbound after it, with actorId and the automation flag available for filtering. Cost of not doing it: one tenant under-reports by 27%, and because the error scales with AI usage it stays invisible at low-AI stores and will not surface in spot checks.vin + dealership and read the vehicle condition (a measured 0.5% of sold deals fail to join). Caveat: the Flutter enum silently coerces an unknown condition to “used”.