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.
Filter Dimensions
Section titled “Filter Dimensions”| Filter | Type | UI Component | API Parameter |
|---|---|---|---|
| State | Multi-select pills | PlanStateFilterBar | states (comma-separated) |
| Template | Searchable dropdown | SearchableSelect | template_id (UUID) |
| Contact | Searchable dropdown | SearchableSelect | contact_id (UUID) |
| Time range | Preset/custom picker | TimeRangeSelector | time_range (enum) |
API: GET /api/plans
Section titled “API: GET /api/plans”The get_plans_api endpoint accepts filter query parameters:
GET /api/plans?states=pending_review,executing&template_id=550e8400-...&time_range=last_7_days| Parameter | Type | Default | Description |
|---|---|---|---|
states | String | "" (all) | Comma-separated status tags to include |
template_id | Option<String> | None | Plan template UUID |
contact_id | Option<String> | None | Contact UUID |
time_range | TimeRange | AllTime | Date range filter on created_at |
Handler: src/mods/plan/api/get_plans_api.rs
Server-Side Query Logic
Section titled “Server-Side Query Logic”Each filter adds a condition to the SQL query:
// State filter — JSONB status fieldif !state_list.is_empty() { query = query.filter( Expr::cust("state->>'status'").is_in(state_list), );}
// Template filter — direct column matchif 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 junctionif 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 boundsif 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.
API: GET /api/plans/contact-options
Section titled “API: GET /api/plans/contact-options”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
Filter Types
Section titled “Filter Types”PlanListFilters
Section titled “PlanListFilters”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.
TimeRange
Section titled “TimeRange”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.
UI Components
Section titled “UI Components”PlanStateFilterBar
Section titled “PlanStateFilterBar”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
PlanListDropdownFilters
Section titled “PlanListDropdownFilters”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
Integration in PlanListView
Section titled “Integration in PlanListView”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
File Reference
Section titled “File Reference”| File | Purpose |
|---|---|
src/mods/plan/api/get_plans_api.rs | Filter query parameters and server-side SQL |
src/mods/plan/api/get_plan_contact_options_api.rs | Contact options endpoint |
src/mods/plan/types/plan_list_filters_type.rs | PlanListFilters struct |
src/mods/plan/components/plan_list_filter_bar_component.rs | PlanStateFilterBar and PlanListDropdownFilters |
src/mods/plan/views/plan_list_view.rs | Filter state management and reactive data fetching |
src/shared/types/time_range_type.rs | TimeRange enum and bounds helpers |
src/ui/searchable_select_ui.rs | SearchableSelect component |