User Interface medium complexity mobile
1
Dependencies
0
Dependents
1
Entities
0
Integrations

Description

A stateful multi-select widget presenting the four fixed expense categories (kilometers driven, tolls, parking, public transit) as discrete tappable option cards. Reactively disables incompatible options when a selection is made, providing immediate visual feedback on mutual exclusion constraints.

Feature: Expense Type Selection with Mutual Exclusion

expense-type-picker-widget

Summaries

The Expense Type Picker Widget is the primary decision point in the expense submission flow, directly affecting how quickly and correctly peer mentors can claim reimbursement. By presenting the four expense categories as clear, tappable option cards and automatically disabling incompatible combinations with plain-language explanations, the widget eliminates a common source of submission errors — claiming both mileage and public transit for the same trip leg. Fewer errors mean fewer rejected claims, less administrative back-and-forth, and faster reimbursement cycles. This translates directly into improved peer mentor satisfaction and retention, which is critical to the organization's service delivery model.

This is a medium-complexity UI component with two direct dependencies: the Expense Type Option Card sub-component and the Expense Selection BLoC. Both must be completed before this widget can be integrated and tested end-to-end. The mutual exclusion logic sits in the BLoC, so UI work can proceed in parallel with BLoC development using mock state. Accessibility is a delivery requirement — the disabled reason tooltip and screen reader semantics must be tested with TalkBack and VoiceOver, which requires physical device testing time.

The Mutual Exclusion Hint Banner sub-component introduces animation, which should be scoped conservatively and tested across older Android/iOS devices to avoid frame drops. Plan for a dedicated accessibility review pass before release.

ExpenseTypePickerWidget is a StatefulWidget that delegates selection state entirely to the Expense Selection BLoC via a ValueChanged onToggle callback, keeping itself stateless regarding business logic. It renders four ExpenseTypeOptionCard instances in a grid or column, passing each card its isSelected and isDisabled flags derived from the BLoC's getSelectedTypes() and getDisabledTypes() streams. The _isDisabled(type) helper queries the BLoC state to determine render mode. On tap, _onOptionTap calls onToggle, which propagates to the BLoC.

The MutualExclusionHintBanner is conditionally rendered below the grid based on whether any disabledTypes are non-empty, with an AnimatedSwitcher for smooth appearance. Use Semantics widgets on each card to expose the disabled reason as a semantic label. BLoC integration should use flutter_bloc's BlocBuilder or Riverpod's ConsumerWidget pattern for reactive rebuilds on state change.

Responsibilities

  • Render four fixed expense type option cards
  • Reactively disable incompatible options on selection change
  • Display selected/deselected/disabled states with accessible contrast
  • Emit selection change events to BLoC/Riverpod state

Interfaces

ExpenseTypePickerWidget({required List<ExpenseType> selectedTypes, required ValueChanged<ExpenseType> onToggle})
build(BuildContext context)
_isDisabled(ExpenseType type)
_onOptionTap(ExpenseType type)

Relationships

Dependencies (1)

Components this component depends on

Sub-Components (2)

Expense Type Option Card
component low

Individual selectable card representing a single expense category with icon, label, and visual state (selected, unselected, disabled). Communicates disabled reason via tooltip for accessibility.

  • Display expense type icon and label
  • Render selected, unselected, and disabled visual states
  • Provide accessible semantics and disabled reason for screen readers
  • +1 more
Mutual Exclusion Hint Banner
component low

Inline contextual hint that appears below the picker when a selection causes other options to be disabled. Explains in plain language why certain combinations are not allowed (e.g., 'Mileage reimbursement and public transit cannot be combined for the same trip leg').

  • Show contextual explanation when mutual exclusion is triggered
  • Hide when no exclusions are active
  • Animate in/out smoothly on state change

Related Data Entities (1)

Data entities managed by this component