Skip to main content

Reports & Analytics

Audience: Sales · Management · Support · QA · Engineering · Where in app: Sidebar → Reports (/reports) · Plan availability: All plans — gated by the reports.view permission

Restructured (2026-07, Agent K beta-gated). /reports renders ReportsHome.vue, which switches on company.agent_k_beta_enabled:

  • Beta companies get the deep six-tab analytics (frontend/src/views/Reports/Index.vue — Overview / Hot Leads / Conversions / Traffic Sources / Auto Engage (the hooks tab, relabeled) / Engagement), moved here from /dashboard when that became the "Today on Knock Knock" command center. Documented under Dashboard.
  • Non-beta companies keep the legacy Reports screen (ReportsView.vue) documented below.

Legacy Reports is a single full-width screen that plots your core engagement metrics — missed calls, chats, video/audio calls, and unique visitors — as trending cards over a date range you choose (24h, 7/30/90 days, or a custom window). It is the "zoom in on one number over time" view, as opposed to the Dashboard, which is the multi-tab operational overview. Reports pulls all of its numbers from a single backend endpoint (GET /reports).

What it does

  • Shows eight metric cards plus two large charts, each with a count for the selected period and a sparkline/trend.
  • Lets the viewer change the date window (24h, 7 days, 30 days, 90 days, or a custom start/end range). All cards update together.
  • Persists the active filter in the URL query string (?dateFilter=...&startDate=...&endDate=...), so a report view is shareable/bookmarkable and survives refresh.
  • Scopes automatically to the signed-in user's company (admins/impersonation can pass a company_id).

This screen is read-only. It does not export files, schedule emails, or drill into individual records — for that, see Related.

How it works

The view (ReportsView.vue) calls the useReportsQuery composable, which:

  1. Converts the chosen dateFilter into an absolute { start, end } ISO range (getDateRange). For the preset filters the range is "now minus N" to "now"; for custom it uses the picked dates (falling back to 7 days if the custom range is incomplete).
  2. Fetches GET /reports?start_date=<iso>&end_date=<iso> via reportsRepository.getDashboardReports.
  3. Caches the result for 5 minutes (staleTime), keyed by the date range — re-selecting the same window does not re-hit the server.
  4. Transforms each metric's time-series array into chart buckets (transformTimeSeriesData) and builds x-axis labels (getChartCategories).

For 24h, data is bucketed into 8 three-hour slots (labels like 00:00, 03:00, …). For longer ranges, data is bucketed per day and the chart shows the last 8 days/labels when there are more than eight.

Metrics shown

Every card maps a value plus a *_stats time series from the /reports response:

Card (UI label)Response fieldMeaning
Calls Missedmissed_callsVisitor-initiated calls (created_by: 'user') that were never started/answered and were not chats.
Chat from Useruser_messagesConversations where a real visitor (not chatbot) sent a message.
Chatbot Conversationchatbot_conversationsConversations handled by the AI chatbot.
Forwarded Phone Callsforwarded_as_phone_callsService rooms flagged forwarded_as_phone_call: true (calls handed off to an external number).
Video Callsvideo_callsVideo calls.
Audio Callsaudio_callsVoice-only calls.
UserssessionsVisitor sessions in the period (labeled "Users" in the UI).
Total Chats (large chart)user_messages + chatbot_conversationsComputed client-side by combining the two chat series.
Total Video Calls (large chart)video_callsRe-plots the video-call series at full width.

The card titled Users is sourced from the session count (sessions), not a distinct-person count. Treat it as "sessions in range." Whether it represents unique people is verify against UserSessionModel semantics.

Date controls

ControlBehavior
CustomOpens a DateRangePicker; applying sets dateFilter = 'custom' and stores start/end.
24h / 7 days / 30 days / 90 daysSets the preset and clears any custom range.
Default on load7days, unless a valid dateFilter is present in the URL.

Configuration & options

There are no saved settings on this screen — every option is a transient view control:

OptionValuesNotes for QA/Support
Date filter24h, 7days, 30days, 90days, customInvalid/unknown values in the URL are ignored; the screen falls back to 7days.
Custom rangeany start/end dateIf only a partial custom range is set, the query silently uses the last 7 days.

States to verify when supporting a user:

  • Loading — full-screen spinner ("Loading reports…") shown only on first load while metrics is empty.
  • Error — a "Failed to load reports" panel with a Try Again button (refetch). Triggered by a non-200 from /reports.
  • Empty data — cards render 0 and flat charts rather than erroring.

Behaviors & edge cases

  • 5-minute cache: counts can lag reality by up to ~5 minutes per the React-Query stale time; "Try Again" / re-navigating after the window refetches.
  • 24h timezone handling: the 24h bucketing applies the browser's UTC offset to each stat's timestamp, so the hour slots reflect the viewer's local clock, not the company timezone — verify when a customer reports off-by-hours buckets.
  • Last-8-points truncation: for ranges longer than 8 days the chart only labels/plots the last 8 daily buckets; the headline value is still the full-period total. This is expected, not a bug.
  • Totals are derived client-side: "Total Chats" is user_messages + chatbot_conversations computed in the browser; it is not a separate backend field.
  • Permission gate: the route requires reports.view. A user without it should not see the Reports sidebar entry/route.

Plan & limits

Available on all plans, gated only by the reports.view role permission (see route meta in router/index.js). There are no per-plan caps on date range or metric availability in the code reviewed. Any plan-tier marketing claims beyond the permission gate are verify with product/pricing.

Technical implementation

  • Route switcher: frontend/src/views/ReportsHome.vue (beta → Reports/Index.vue, else legacy)
  • Legacy view: frontend/src/views/ReportsView.vue
  • Data composable: frontend/src/composables/useReportsQuery.js (getDateRange, getChartCategories, transformTimeSeriesData, combineChartData)
  • Repository: frontend/src/repositories/Reports/index.jsGET /reports?start_date&end_date
  • Components: StatCard.vue, LargeChart.vue, DateRangePicker.vue
  • Route: name reports, path /reports, meta { tab: 'reports', permission: 'reports.view' } (frontend/src/router/index.js)
  • Backend endpoint: GET /reportsReportController@index (backend/app/routes/reportRoutes.js, backend/app/controllers/ReportController.js). It counts documents and builds interval series (StatsHelper.intervalStats) over ServiceRoomModel, ChatModel, and UserSessionModel, scoped by company_id. Admins must pass company_id; web-owners/team members resolve the company from their user record.
  • Related backend routes (not used by this screen): the richer /stats/* and StatsV2 endpoints (backend/app/routes/statsRoutes.js, statsV2Routes.js) power the Dashboard tabs, not Reports. See services/backend.

Naming note: ReportController@index is internally documented as the "Dashboard endpoint," but in the app it backs the Reports screen. The product Dashboard tabs use the /stats/dashboard/* endpoints instead.

  • Dashboard — multi-tab operational overview (different data source: /stats/dashboard/*).
  • Calls and Live Chat — drill into the individual conversations behind these counts.
  • Sessions and Leads — act on the visitors represented by the "Users"/sessions metric.
  • services/backend — the report/stats controllers and StatsHelper.