Skip to main content

The shared filter model

Every Analyse screen (Dashboards, Responses, Verbatim, KPI, Reports, Alerts) shares one filter set, surfaced in the top bar by AnalyseFilterBar and provided by FilterProvider. This lives in frontend/src/lib/filters.tsx.

FilterState

The raw, user-facing filter values:

interface FilterState {
surveyIds: string[]; // empty = all accessible surveys
rangeId: string; // 'all' | 'today' | 'week' | 'month' | '7' | '30' | '90' | '365' | 'custom'
fromDate: string; toDate: string; // YYYY-MM-DD, for custom ranges
channels: string[]; // empty = all
agentIds: string[]; // empty = all
subscriberId: string;
dimKey: string; dimVal: string; // agent-dimension filter
granularity: 'day' | 'week' | 'month';
operator: 'and' | 'or'; // how the segmentation group combines
alertedOnly: boolean; completedOnly: boolean; withNotesOnly: boolean;
alertKind: string; // Alerts screen only
}

Surveys / channels / agents are multi-select, and an empty selection means "all" (everything the user can access). The provider also loads the reference lists once on mount — listSurveys, listSubscribers, listAgents — and shares them; ready flips true when they've loaded.

FilterProvider / useFilters()

FilterProvider (mounted in AppShell) holds the state and exposes a FilterContextValue that extends FilterState with the reference lists plus derived helpers and mutators:

  • Mutators: set(key, value), setMany(patch), apply(partial) (lenient coercion, used when restoring a saved View), clearAdvanced(), snapshot().
  • Derived single-value back-compat: surveyId (first selected, else first accessible), channel, agentId, multiSurvey.
  • Derived range: days (the active window as a day count, undefined for all-time), computed by resolveRange / rangeDays — which turn the preset ids into concrete from/to dates (e.g. week = Monday-to-today, month = first-of-month to today).
  • dimKeys / dimValues: the available agent-dimension keys and the values for the selected key, derived from the loaded agents.
  • activeCount: how many "advanced" filters are active (beyond surveys + date range), for the filter-bar badge.

useFilters() is the hook; it throws if used outside a FilterProvider.

reportFilter

The bridge from UI state to API calls is reportFilter: ReportFilterParams — the shape the typed client serialises into query strings (reportQuery in lib/api.ts). The provider maps state into it, collapsing "empty = all" into undefined and emitting both multi and single forms for back-compat:

const reportFilter: ReportFilterParams = {
days: range.days, from: range.from, to: range.to,
surveyIds: state.surveyIds.length > 0 ? state.surveyIds : undefined,
channels: state.channels.length > 0 ? state.channels : undefined,
channel: state.channels.length === 1 ? state.channels[0] : undefined,
agentIds: state.agentIds.length > 0 ? state.agentIds : undefined,
agentId: state.agentIds.length === 1 ? state.agentIds[0] : undefined,
subscriberId: state.subscriberId || undefined,
dimensionKey: state.dimKey || undefined, dimensionValue: state.dimVal || undefined,
operator: state.operator === 'or' ? 'or' : undefined,
alerted: state.alertedOnly || undefined,
completed: state.completedOnly || undefined,
withNotes: state.withNotesOnly || undefined,
};

A page reads const { reportFilter } = useFilters() and threads it into the relevant client call — e.g. api.reportNps(reportFilter) (tenant-wide, multi-survey) or api.nps(surveyId, reportFilter) (single survey). The serialiser repeats multi-value keys (?channel=a&channel=b, ?surveyId=…), prefers absolute from/to over days, and omits defaults.

Channel labels

CHANNEL_LABELS / channelLabel(c) are the canonical display names (online → "Web", ivr_inbound → "IVR Inbound", …), and CHANNELS_SORTED orders the selector by display label. Reuse these anywhere a channel is shown so labelling stays consistent. ALERT_KIND_LABELS does the same for alert trigger kinds on the Alerts screen.