CRM Analytics — UI Screens & Flows
Frontend documentation for the CRM Analytics dashboard: component hierarchy, state management, chart interactions, and the cross-filtering drilldown pattern.
Routes
| Path | Component | Notes |
|---|---|---|
/crm | index.vue → AnalyticsView | Default landing view when the Analytics tab is active in the CRM page |
/crm/forecast | forecast/index.vue | Deprecated — immediately redirects to /crm |
The analytics view is mounted as one of the view-switcher tabs inside frontend/client/src/views/crm/index.vue. It is rendered with v-if (not keep-alive), so the component is fully destroyed and re-mounted on each tab switch.
Component Hierarchy
index.vue (CRM root)
└── AnalyticsView.vue # Locale bridge + event bus listener
└── ClientAnalyticsView.vue # Full dashboard: filters, KPIs, charts, table
├── KpiCard.vue # KPI card with trend badge
├── Card.vue # Bootstrap wrapper card
├── Chart.vue # Highcharts wrapper
├── Select2.vue # Salesman + tier multi-select dropdowns
└── VueDatePicker # Date range picker with presetsAnalyticsView.vue
Responsibilities:
- Reads the active locale from the
_slocalecookie on mount. - Subscribes to the
changeLocaleevent bus event and re-exposes the locale ID as a prop toClientAnalyticsView. - Exposes a
reload()method so the parent tab switcher can trigger a data refresh without unmounting.
Props passed to ClientAnalyticsView:
| Prop | Type | Description |
|---|---|---|
currentLocaleId | Number | null | Active locale ID from the cookie, or null for "all locales" |
ClientAnalyticsView.vue
All dashboard logic lives here: filters, API calls, chart option builders, and drilldown interactions.
Injected: $sidepanel — the shared CRM sidepanel reference, used to open a lead detail from the active leads table.
Key reactive state:
| Ref | Type | Description |
|---|---|---|
dateRange | [Date, Date] | Currently selected date range |
selectedSalesman | string | null | Selected executive ID (from by_executive list) |
selectedTiers | string[] | Active tier filters (array of numeric string values '0'–'5') |
selectedStage | string | null | Active virtual stage or pipeline stage name |
selectedProduct | string | null | Active product translation tag |
analyticsData | object | Full API response, initialized to empty shape |
loading | boolean | Controls the spinner overlay |
Dashboard Sections
1. Filter Bar
Located at the top of the dashboard. Filters are independent controls; any change immediately calls loadData().
| Control | Component | Behavior |
|---|---|---|
| Executive | Select2 (single-select, searchable) | Filters by executive_id. Options populated from analyticsData.by_executive (returned by the API with locale filter only). |
| Tier | Select2 (multi-select tags) | Allows selecting multiple tiers (0–5). Values sent as comma-separated string to tier query param. |
| Date range | VueDatePicker (multi-calendar range, auto-apply) | YYYY-MM-DD format. Preset dates: Today, Last 30 Days, This Month, Last 3 Months, Last 6 Months. |
The locale filter is not a user-visible dropdown — it is driven by the global locale cookie (_slocale) and passed in via the currentLocaleId prop.
2. Active Filters Pill Bar
Visible only when at least one interactive filter is active (selectedStage, selectedProduct, selectedSalesman, or selectedTiers). Appears between the filter bar and the KPI cards.
Each active filter renders as a dismissible pill. Clicking the × on a pill removes that specific filter and reloads. A "Clear all filters" button removes all interactive filters at once.
3. KPI Cards
Four KpiCard components arranged in a grid row.
| Card | Value source | Trend badge |
|---|---|---|
| Total Leads | analyticsData.kpis.total_leads.current | (current - previous) / previous * 100 — green when positive, red when negative |
| Total Shipments | analyticsData.kpis.total_shipments.current | Same trend logic |
| Avg Shipment Cost | analyticsData.kpis.avg_shipment_cost.current (formatted as USD) | Same trend logic |
| Total Lost | analyticsData.kpis.total_lost.current (shown in red) | Inverted: positive delta = red, negative = green (fewer losses is better) |
The "Total Lost" card is clickable — clicking it sets selectedStage = 'customer_lost' and reloads, acting as a shortcut to the lost-leads drilldown.
4. Sales Funnel + Outcome Charts
Two cards rendered side by side.
Left card — Sales Funnel table:
- Renders
analyticsData.sales_funnelas a sortable table with stage name, count, and an inline bar showing percentage. - Each row is clickable: toggles
selectedStage(second click on the same stage clears it) and reloads. - The "active" row gets a
table-activehighlight class. - Stats footer below the table shows three extra KPIs inline:
avg_closing_days— average days to closeadvancement_rate— percentage of leads that won or recharged (green ≥ 50%, yellow < 50%)in_recharge— count of leads with a recharge but no shipment yet
Right card — Outcome Pie chart (Highcharts pie):
- Four slices: Won (green
#22C55E), Lost (red#EF4444), In Recharge (amber#F59E0B), Remaining (grey#6B7280). - Clicking a slice toggles the corresponding
selectedStagefilter (clicking "Remaining" clears the stage filter instead).
5. Salesman By Stage Chart
A Highcharts stacked horizontal bar chart (type: 'bar'). Height scales dynamically: max(250, executives * 40 + 80).
- Three stacked series: Won (green), Lost (red), In Progress (grey).
- Sorted by total (won + lost + remaining) descending.
- Clicking any bar segment filters by the salesman name (
onSalesmanClick).
6. Active Tiers & Product Breakdown
Two charts in a 4:8 column layout.
Tier chart (Highcharts column, left):
- X-axis: tiers in canonical order
blank, T0, T1, T2, T3, T4, T5(active leads only — excludes won/lost). - Clicking a column toggles that tier in
selectedTiers(multi-select supported). - When a tier is selected, unselected columns become light grey (
#CBD5E1), the selected column retains blue with a border.
Product chart (Highcharts column, right, dual-axis):
- Top 10 products by
total_value. - Left Y-axis: total account value in USD (formatted as
$Xk). - Right Y-axis: lead count.
- Two series per product: total value (purple
#8B5CF6) and lead count (green#10B981). - Clicking a column toggles
selectedProduct. - When a product is selected, other products fade to 30% opacity.
7. Active Leads Quick View
A table showing the top 100 oldest open leads (not won, not lost), sorted by age_days descending (sortedActiveLeads computed).
| Column | Drilldown behavior |
|---|---|
| Company | Display only |
| Executive | Clickable — filters by that salesman (onSalesmanClickByName) |
| Tier | Clickable — toggles tier in selectedTiers |
| Stage | Clickable badge — toggles selectedStage |
| Age | Display only (days since creation) |
| Eye icon | Opens the CRM Sidepanel for that lead |
Cross-Chart Drilldown Pattern
Every interactive element on the dashboard shares the same state machine. This is the core UX mechanic of the analytics view.
Click handlers:
| Handler | Triggered by | Action |
|---|---|---|
onStageClick(stageName) | Funnel table row, outcome pie slice, lost KPI card, active leads stage badge | Toggle selectedStage |
onSalesmanClick(name) | Executive bar chart, active leads executive cell | Resolve name → ID, toggle selectedSalesman |
onTierClick(tierKey) | Tier column chart, active leads tier cell | Add/remove tierKey from selectedTiers array |
onProductClick(productName) | Product chart columns | Toggle selectedProduct |
Stale filter auto-clear: after each loadData(), if selectedSalesman is no longer in by_executive, or selectedStage is not in sales_funnel, or selectedProduct is not in by_product, the corresponding filter is automatically cleared. This prevents the dashboard from being stuck in an impossible state when the new dataset is smaller.
Important: any active interactive filter (selectedSalesman, selectedTiers, selectedStage, selectedProduct) bypasses the snapshot cache. The API always calls computeCrmAnalytics() live.
Data Loading
loadData() is the single function responsible for all API calls. It runs on:
- Component mount (
onMounted) - Any filter change
- Locale change (via
changeLocaleevent propagated fromAnalyticsView) - External call via the exposed
reload()method
const loadData = async () => {
loading.value = true;
const params = {
startDate, endDate, // from dateRange
localeIds, // from currentLocaleId prop
salesman, tier, stage, product // from selected* refs (omitted when null/empty)
};
analyticsData.value = await api.prospects.getClientAnalytics(params);
// auto-clear stale filters
};Lead Score Badge
The lead score badge is not rendered inside ClientAnalyticsView itself, but it is used across the CRM module (Kanban cards, list view rows) using the shared helper from frontend/client/src/views/crm/constants/leadScore.constants.js.
import { getScoreBadgeClass } from '@/views/crm/constants/leadScore.constants';
// Usage
<span class="badge" :class="`bg-${getScoreBadgeClass(lead.score)}`">
{{ lead.score }}
</span>| Return value | CSS class | Displayed as |
|---|---|---|
'success' | bg-success | Green |
'warning' | bg-warning | Yellow |
'info' | bg-info | Blue |
'secondary' | bg-secondary | Grey |
Thresholds: score >= 8 → success, score >= 5 → warning, score >= 3 → info, else secondary.
Related Documentation
- Module Overview — Materialized table ETL, query formulas, and key decisions
- API Endpoints —
GET /prospects/client-analyticsparameter and response reference - CRM UI — Other CRM views: Kanban, List, Calendar, Sidepanel
- User Guide — End-user instructions for using the dashboard
