Skip to content

Plan List Filters

The plan list view supports four filter dimensions — all applied server-side in SQL for performance. Users combine state pills, template/contact dropdowns, and a time range selector to narrow results.

FilterTypeUI ComponentAPI Parameter
StateMulti-select pillsPlanStateFilterBarstates (comma-separated)
TemplateSearchable dropdownSearchableSelecttemplate_id (UUID)
ContactSearchable dropdownSearchableSelectcontact_id (UUID)
Time rangePreset/custom pickerTimeRangeSelectortime_range (enum)

The get_plans_api endpoint accepts filter query parameters:

GET /api/plans?states=pending_review,executing&template_id=550e8400-...&time_range=last_7_days
ParameterTypeDefaultDescription
statesString"" (all)Comma-separated status tags to include
template_idOption<String>NonePlan template UUID
contact_idOption<String>NoneContact UUID
time_rangeTimeRangeAllTimeDate range filter on created_at

Handler: src/mods/plan/api/get_plans_api.rs

Each filter adds a condition to the SQL query:

// State filter — JSONB status field
if !state_list.is_empty() {
query = query.filter(
Expr::cust("state->>'status'").is_in(state_list),
);
}
// Template filter — direct column match
if let Some(ref tid) = template_id {
let template_uuid = uuid::Uuid::parse_str(tid).or_bad_request("Invalid template_id")?;
query = query.filter(schemas::plan::Column::PlanTemplateId.eq(template_uuid));
}
// Contact filter — subquery through plan_contact junction
if let Some(ref cid) = contact_id {
let contact_uuid = uuid::Uuid::parse_str(cid).or_bad_request("Invalid contact_id")?;
let contact_subquery = schemas::plan_contact::Entity::find()
.select_only()
.column(schemas::plan_contact::Column::PlanId)
.filter(schemas::plan_contact::Column::ContactId.eq(contact_uuid))
.into_query();
query = query.filter(schemas::plan::Column::Id.in_subquery(contact_subquery));
}
// Time range filter — created_at bounds
if let Some(start) = time_range_start(&time_range) {
query = query.filter(schemas::plan::Column::CreatedAt.gte(start));
}

Results are ordered with actionable states first (pending_review, awaiting_input), then by created_at descending, capped at 500 plans.

Returns contacts associated with plans in the current organization. Used to populate the contact filter dropdown.

#[get("/api/plans/contact-options")]
pub async fn get_plan_contact_options_api() -> Result<Vec<PlanContactSummary>, HttpError>

Returns Vec<PlanContactSummary>:

pub struct PlanContactSummary {
pub id: uuid::Uuid,
pub first_name: String,
pub last_name: String,
}

Handler: src/mods/plan/api/get_plan_contact_options_api.rs

Client-server shared filter state, defined in src/mods/plan/types/plan_list_filters_type.rs:

pub struct PlanListFilters {
pub states: Vec<String>,
pub template_id: Option<String>,
pub contact_id: Option<String>,
pub time_range: TimeRange,
}

Default sets states to empty (all states), both IDs to None, and time_range to AllTime.

Shared enum from src/shared/types/time_range_type.rs:

pub enum TimeRange {
Today,
Yesterday,
Last7Days,
Last30Days,
Last90Days,
AllTime,
Custom { start: String, end: String },
}

Helper functions time_range_start() and time_range_end() convert variants to NaiveDateTime bounds.

Six pill buttons for state filtering. Active pills use color-coded badges; inactive pills show at 60% opacity. A Clear all button appears when any filter is active.

const STATE_KEYS: &[&str] = &[
"pending_review", "stand_by", "awaiting_input",
"executing", "completed", "failed",
];

File: src/mods/plan/components/plan_list_filter_bar_component.rs

Three dropdowns arranged horizontally: Template, Contact, Time Range. Template and contact dropdowns use SearchableSelect with client-side text search. Options load via use_resource from their respective API endpoints.

File: src/mods/plan/components/plan_list_filter_bar_component.rs

The view holds a use_signal(PlanListFilters::default) and passes it to both filter components. A use_resource re-fetches plans whenever the signal changes:

let mut plans = use_resource(move || {
let f = filters();
async move {
get_plans_api(f.states.join(","), f.template_id, f.contact_id, f.time_range).await
}
});

When no plans match the active filters, an empty state message displays: “No plans match the current filters.”

File: src/mods/plan/views/plan_list_view.rs

FilePurpose
src/mods/plan/api/get_plans_api.rsFilter query parameters and server-side SQL
src/mods/plan/api/get_plan_contact_options_api.rsContact options endpoint
src/mods/plan/types/plan_list_filters_type.rsPlanListFilters struct
src/mods/plan/components/plan_list_filter_bar_component.rsPlanStateFilterBar and PlanListDropdownFilters
src/mods/plan/views/plan_list_view.rsFilter state management and reactive data fetching
src/shared/types/time_range_type.rsTimeRange enum and bounds helpers
src/ui/searchable_select_ui.rsSearchableSelect component