Skip to content

Dashboard

The dashboard module provides a consolidated view of organization activity. It displays KPIs and charts across calls, SMS, email, text agents, plans, AI usage, and contact engagement — all filtered by a shared time range.

pub struct DashboardStats {
pub total_calls: i64,
pub total_duration_secs: i64,
pub avg_duration_secs: f64,
pub total_todos: i64,
pub call_activity: Vec<CallActivityDay>,
pub call_heatmap: Vec<HeatmapCell>,
pub call_heatmap_max: i64,
pub recent_calls: Vec<Call>,
pub todo_status_counts: Vec<TodoStatusCount>,
}
pub struct MessagingStats {
pub total_messages: u32,
pub delivery_rate: f64, // 0.0–100.0
pub inbound_count: u32,
pub outbound_count: u32,
pub message_activity: Vec<MessageActivityBucket>,
pub message_heatmap: Vec<HeatmapCell>,
pub message_heatmap_max: u32,
pub response_insights: ResponseInsights,
}

MessageActivityBucket breaks down each time bucket by channel:

pub struct MessageActivityBucket {
pub date: String, // Bucket timestamp
pub label: String, // Display label
pub sms_count: u32,
pub email_count: u32,
}

Detailed response-time analytics computed from inbound→outbound message pairs:

pub struct ResponseInsights {
pub avg_response_time_secs: Option<f64>,
pub unanswered_count: u32,
pub response_rate: f64, // 0.0–100.0
pub unresponded_by_hour: Vec<UnrespondedHourBucket>,
pub by_phone: Vec<PhoneResponseBreakdown>,
}
pub struct PhoneResponseBreakdown {
pub phone_label: String, // Friendly name or E.164 number
pub responded_count: u32,
pub unanswered_count: u32,
pub response_rate: f64,
pub avg_response_time_secs: Option<f64>,
}

Response times use a single-pass algorithm: messages sorted by (contact_id, created_at), pairing each outbound with the preceding inbound for the same contact. Unpaired inbound messages count as unanswered.

pub struct TextAgentStats {
pub total_suggestions: u32,
pub auto_reply_count: u32,
pub auto_reply_rate: f64, // Percentage auto-sent
pub avg_confidence: f64, // 0.0–1.0
pub escalation_count: u32,
pub escalation_rate: f64,
}
pub struct PlanStats {
pub active_plans: u32,
pub contacts_in_active_plans: u32,
pub completion_rate: f64, // (completed / terminal) × 100
pub total_plans: u32,
}

Plan stats have no time-range filter — they reflect current state.

pub struct AiUsageStats {
pub total_tokens: u64,
pub total_cost: f64,
pub cost_by_feature: Vec<FeatureCost>, // Sorted by cost desc
}
pub struct FeatureCost {
pub feature: String, // Prettified feature name
pub cost: f64,
}
pub struct EngagementStats {
pub active_contacts: u32, // Contacts with calls or messages
pub total_contacts: u32,
pub channel_distribution: ChannelDistribution,
pub recent_activities: Vec<RecentActivity>, // Max 10
}
pub struct ChannelDistribution {
pub calls: u32,
pub sms: u32,
pub email: u32,
pub other: u32,
}
pub struct RecentActivity {
pub id: Uuid,
pub activity_type: ActivityType, // Call | Message
pub contact_id: Option<Uuid>,
pub contact_name: Option<String>,
pub description: String,
pub channel: String,
pub created_at: String,
}
MethodPathDescription
GET/api/dashboard/stats?time_rangeCall KPIs, activity chart, heatmap, recent calls, to-do counts
GET/api/dashboard/messaging-stats?time_rangeMessaging KPIs, activity, heatmap, response insights
GET/api/dashboard/text-agent-stats?time_rangeText agent suggestions, auto-reply rate, confidence
GET/api/dashboard/plan-statsActive plans and completion rate (no time filter)
GET/api/dashboard/ai-usage-stats?time_rangeToken totals, cost breakdown by feature
GET/api/dashboard/ai-message-origin-stats?time_rangeAI-composed message counts by feature — see AI Origin Tracking
GET/api/dashboard/engagement-stats?time_rangeActive contacts, channel distribution, recent activity feed

All endpoints require an authenticated session. Every handler calls resolve_dashboard_scope() to determine data visibility.

pub enum DashboardScope {
Org,
Scoped { phone_ids: Vec<Uuid>, member_id: Uuid },
}

resolve_dashboard_scope() checks the session’s permissions in order:

  1. Super-admin / org ownerDashboardScope::Org (bypass)
  2. Dashboard:Instance:ViewOrgDashboardScope::Org (org-wide metrics, no phone filter)
  3. Dashboard:Instance:ViewOwnDashboardScope::Scoped (filtered to the member’s assigned phones)
  4. Neither403 Forbidden

Helper methods on DashboardScope:

MethodReturns
phone_ids_slice()Option<&[Uuid]>None for org-wide, Some for scoped
scoped_member_id()Option<Uuid> — the member ID for scoped users

The dashboard view uses can_view_dashboard() to gate rendering before any data loads:

pub fn can_view_dashboard(session: &ClientSession) -> bool {
session.has_permission(&Permission::Dashboard(
DashboardPermission::Instance(DashboardInstancePermission::ViewOrg),
)) || session.has_permission(&Permission::Dashboard(
DashboardPermission::Instance(DashboardInstancePermission::ViewOwn),
))
}

If false, the view renders an AccessDenied card instead of the dashboard content

The TimeRangeSelector popover provides preset options (Today, Yesterday, Last 7 Days, Last 30 Days, Last 90 Days, All Time) and custom date range inputs. Changing the range triggers a refetch of all time-scoped endpoints. Plan stats are unaffected.

The dashboard fires seven independent API calls in parallel when it mounts:

get_dashboard_stats_api(time_range) → DashboardStats
get_messaging_stats_api(time_range) → MessagingStats
get_text_agent_stats_api(time_range) → TextAgentStats
get_plan_stats_api() → PlanStats
get_ai_usage_stats_api(time_range) → AiUsageStats
get_ai_message_origin_stats_api(time_range) → AiMessageOriginStats
get_engagement_stats_api(time_range) → EngagementStats

Each section loads independently with skeleton states. Sections render as data arrives — no waterfall.

ComponentDescription
KpiCardsTotal calls, total duration, avg duration, total to-dos
CallActivityChartBar chart of daily call counts
Heatmap (red)7×24 grid of calls by day/hour
RecentCallsTable with status, duration, recording playback
TodoStatusTo-do breakdown by status
ComponentDescription
MessagingKpiCardsTotal messages, delivery rate, inbound/outbound split
MessageActivityChartStacked bar chart — SMS (blue) + Email (emerald) per bucket
Heatmap (blue)7×24 grid of messages by day/hour
MessagingResponseInsightsAvg response time, unanswered count, per-phone breakdown table

The response insights card color-codes rates: green (good), amber (warning), destructive (poor). The unanswered count links to the messaging view filtered for unanswered conversations.

ComponentDescription
TextAgentPerformanceSuggestions count, auto-reply rate, avg confidence, escalation rate
PlanMetricsActive plans, contacts in progress, completion rate
AiUsageOverviewTotal tokens + cost, horizontal bar breakdown by feature
AiMessageOriginAI-composed message count, AI share %, bar chart by feature (details)
ComponentDescription
ChannelDistributionChartCSS donut chart using conic-gradient — calls (amber), SMS (blue), email (emerald), other (orange)
RecentActivityFeedUnified feed of calls + messages (max 10), clickable rows linking to detail views
QuickActionsShortcuts: add phone number, create agent, upload document

The dashboard view arranges sections top to bottom:

  1. Time range selector (header actions slot)
  2. Call KPI cards + Messaging KPI cards
  3. Response Insights (full-width)
  4. Call Activity Chart + Message Activity Chart (side-by-side)
  5. Call Heatmap (red) + Message Heatmap (blue)
  6. Text Agent + Plan Metrics + AI Usage (3-column grid)
  7. Recent Activity Feed + Task Status (2-column grid)
  8. Channel Distribution Chart

The layout stacks on narrow screens.

The Heatmap component is generic and reusable. It accepts:

#[component]
fn Heatmap(
cells: Vec<HeatmapCell>,
max_count: u32,
title: String,
item_label: String, // "calls" or "messages"
color: String, // RGB like "239, 68, 68" (red) or "59, 130, 246" (blue)
) -> Element

Color intensity is normalized against max_count. The component renders a 7-row × 24-column grid with day-of-week labels and hour labels.

The messaging stats service (get_messaging_stats_service.rs) handles the heaviest computation:

  • SQL aggregation for counts, delivery rates, and direction splits
  • Timezone-aware bucketing via date_trunc with the org’s timezone
  • Heatmap generation using EXTRACT(ISODOW) and EXTRACT(HOUR)
  • Gap filling to ensure continuous time series (zero-fill missing buckets)
  • Response time pairing via single-pass contact-grouped iteration

Query helpers in services/query_helpers.rs provide reusable functions for heatmap building, bucket interval selection, and timezone-safe SQL expressions.

src/mods/dashboard/
├── api/ # Stats endpoints: calls, messaging, text-agent, plan, ai-usage, engagement
├── components/ # KPI cards, charts, heatmaps, response insights, activity feed
├── services/ # get_messaging_stats service + query helpers
├── types/ # All stats types: messaging, text-agent, plan, ai-usage, engagement
└── views/ # DashboardView
  • Call — call data and status
  • Messaging — message records for stats computation
  • Text Agents — suggestion and auto-reply data
  • Todo — to-do status counts
  • SharedTimeRange type and TimeRangeSelector component