From 3ccb582b38e5ae9269a2382803cb80dd6c754476 Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Wed, 15 Apr 2026 16:58:02 +0530 Subject: [PATCH] feat: implement searchable multi-select dropdown and format date display in agent profile edit screen --- .../screens/agent_detail_screen.dart | 8 +- .../screens/agent_edit_profile_screen.dart | 311 +++++++++++++++--- .../screens/agent_home_screen.dart | 6 +- 3 files changed, 275 insertions(+), 50 deletions(-) diff --git a/lib/features/agents/presentation/screens/agent_detail_screen.dart b/lib/features/agents/presentation/screens/agent_detail_screen.dart index 78b87ca..f78d898 100644 --- a/lib/features/agents/presentation/screens/agent_detail_screen.dart +++ b/lib/features/agents/presentation/screens/agent_detail_screen.dart @@ -996,17 +996,15 @@ class _AgentDetailScreenState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 36), child: Column( children: [ - // Availability card (always show) + // Availability card (always show) — only schedule lines, no "Full-time" label _buildInfoCard( icon: Icons.access_time_filled, title: state.availabilityLabel, - subtitle: state.availabilityType.isNotEmpty - ? state.availabilityType - : 'Not specified', + subtitle: state.availabilitySchedule.isEmpty ? 'Not specified' : '', items: state.availabilitySchedule.isNotEmpty ? state.availabilitySchedule : null, - isEmpty: state.availabilityType.isEmpty, + isEmpty: state.availabilitySchedule.isEmpty, ), const SizedBox(height: 16), // Work environment (always show) diff --git a/lib/features/agents/presentation/screens/agent_edit_profile_screen.dart b/lib/features/agents/presentation/screens/agent_edit_profile_screen.dart index e91fe22..58eb60e 100644 --- a/lib/features/agents/presentation/screens/agent_edit_profile_screen.dart +++ b/lib/features/agents/presentation/screens/agent_edit_profile_screen.dart @@ -1160,7 +1160,14 @@ class _AgentEditProfileScreenState String fieldName = 'This field', }) { final val = getValue(); - final displayText = val is String && val.isNotEmpty ? val : placeholder; + // Display as MM-dd-yyyy (US format) while keeping ISO (yyyy-MM-dd) on the wire. + String displayText = placeholder; + if (val is String && val.isNotEmpty) { + final parsed = DateTime.tryParse(val); + displayText = parsed != null + ? DateFormat('MM-dd-yyyy').format(parsed) + : val; + } // Parse admin-configured min/max dates for the picker; fall back to wide range. final minDateStr = validation?['minDate'] as String?; @@ -1511,7 +1518,7 @@ class _AgentEditProfileScreenState } // --------------------------------------------------------------------------- - // MULTI_SELECT (chip group) + // MULTI_SELECT (searchable dropdown — matches web's react-select pattern) // --------------------------------------------------------------------------- Widget _buildMultiSelect( @@ -1522,54 +1529,276 @@ class _AgentEditProfileScreenState ) { final selected = List.from((getValue() ?? []) as List); - return Wrap( - spacing: 8, - runSpacing: 8, - children: options.map((opt) { - final optVal = opt['value'] as String? ?? ''; - final optLabel = opt['label'] as String? ?? optVal; - final isSelected = selected.contains(optVal); + String labelFor(String val) { + final match = options.firstWhere( + (o) => (o['value'] as String? ?? '') == val, + orElse: () => {}, + ); + return (match['label'] as String?) ?? val; + } - return GestureDetector( - onTap: () { - final newList = List.from(selected); - if (isSelected) { - newList.remove(optVal); - } else { - newList.add(optVal); - } - setValue(newList); - }, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Tap target that opens the searchable picker + GestureDetector( + onTap: () => _openMultiSelectPicker( + title: slug, + options: options, + selected: selected, + onChanged: setValue, + ), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( - color: isSelected - ? AppColors.accentOrange.withValues(alpha: 0.1) - : Colors.white, - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: isSelected - ? AppColors.accentOrange - : const Color(0xFFD1D5DB), - ), + color: Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFFD1D5DB)), ), - child: Text( - optLabel, - style: TextStyle( - fontFamily: 'SourceSerif4', - fontSize: 13, - fontWeight: FontWeight.w400, - color: isSelected - ? AppColors.accentOrange - : AppColors.primaryDark, - ), + child: Row( + children: [ + Expanded( + child: Text( + selected.isEmpty + ? 'Select...' + : '${selected.length} selected', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + fontWeight: FontWeight.w300, + color: selected.isEmpty + ? AppColors.hintText + : AppColors.primaryDark, + ), + ), + ), + const Icon(Icons.keyboard_arrow_down, + color: AppColors.hintText), + ], ), ), - ); - }).toList(), + ), + // Selected chips preview (tap × to remove) + if (selected.isNotEmpty) ...[ + const SizedBox(height: 10), + Wrap( + spacing: 8, + runSpacing: 8, + children: selected.map((val) { + return Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: AppColors.accentOrange.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.accentOrange), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + labelFor(val), + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 13, + fontWeight: FontWeight.w400, + color: AppColors.accentOrange, + ), + ), + const SizedBox(width: 6), + GestureDetector( + onTap: () { + final newList = List.from(selected) + ..remove(val); + setValue(newList); + }, + child: const Icon( + Icons.close_rounded, + size: 16, + color: AppColors.accentOrange, + ), + ), + ], + ), + ); + }).toList(), + ), + ], + ], ); } + Future _openMultiSelectPicker({ + required String title, + required List> options, + required List selected, + required void Function(dynamic) onChanged, + }) async { + final searchController = TextEditingController(); + final workingSelected = List.from(selected); + String query = ''; + + await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (ctx) { + return StatefulBuilder( + builder: (ctx, setSheetState) { + final filtered = options.where((o) { + if (query.isEmpty) return true; + final l = (o['label'] as String? ?? '').toLowerCase(); + return l.contains(query.toLowerCase()); + }).toList(); + + return SafeArea( + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(ctx).viewInsets.bottom, + ), + child: FractionallySizedBox( + heightFactor: 0.85, + child: Column( + children: [ + // Drag handle + Container( + width: 40, + height: 4, + margin: const EdgeInsets.only(top: 10, bottom: 14), + decoration: BoxDecoration( + color: const Color(0xFFD1D5DB), + borderRadius: BorderRadius.circular(2), + ), + ), + // Search + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16), + child: TextField( + controller: searchController, + onChanged: (v) => + setSheetState(() => query = v), + decoration: InputDecoration( + hintText: 'Search...', + prefixIcon: const Icon(Icons.search, + color: AppColors.hintText), + filled: true, + fillColor: const Color(0xFFF5F5F5), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + vertical: 0, horizontal: 12), + ), + ), + ), + const SizedBox(height: 8), + Expanded( + child: ListView.builder( + itemCount: filtered.length, + itemBuilder: (_, i) { + final opt = filtered[i]; + final v = + opt['value'] as String? ?? ''; + final l = + opt['label'] as String? ?? v; + final isSelected = + workingSelected.contains(v); + return CheckboxListTile( + value: isSelected, + onChanged: (_) { + setSheetState(() { + if (isSelected) { + workingSelected.remove(v); + } else { + workingSelected.add(v); + } + }); + }, + title: Text( + l, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + color: AppColors.primaryDark, + ), + ), + controlAffinity: + ListTileControlAffinity.leading, + activeColor: AppColors.accentOrange, + dense: true, + ); + }, + ), + ), + Padding( + padding: + const EdgeInsets.fromLTRB(16, 8, 16, 12), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => + Navigator.pop(ctx), + style: OutlinedButton.styleFrom( + padding: + const EdgeInsets.symmetric( + vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(10), + ), + ), + child: const Text('Cancel'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () { + onChanged(workingSelected); + Navigator.pop(ctx); + }, + style: ElevatedButton.styleFrom( + backgroundColor: + AppColors.accentOrange, + padding: + const EdgeInsets.symmetric( + vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(10), + ), + ), + child: const Text( + 'Done', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ); + }, + ); + searchController.dispose(); + } + // --------------------------------------------------------------------------- // TAG_INPUT // --------------------------------------------------------------------------- diff --git a/lib/features/agents/presentation/screens/agent_home_screen.dart b/lib/features/agents/presentation/screens/agent_home_screen.dart index 872bcda..ac12434 100644 --- a/lib/features/agents/presentation/screens/agent_home_screen.dart +++ b/lib/features/agents/presentation/screens/agent_home_screen.dart @@ -1053,13 +1053,11 @@ class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { _buildInfoCard( icon: Icons.access_time_filled, title: state.availabilityLabel, - subtitle: state.availabilityType.isNotEmpty - ? state.availabilityType - : 'Not specified', + subtitle: state.availabilitySchedule.isEmpty ? 'Not specified' : '', items: state.availabilitySchedule.isNotEmpty ? state.availabilitySchedule : null, - isEmpty: state.availabilityType.isEmpty, + isEmpty: state.availabilitySchedule.isEmpty, ), const SizedBox(height: 16), // Work environment