feat: implement searchable multi-select dropdown and format date display in agent profile edit screen

This commit is contained in:
pradeepkumar
2026-04-15 16:58:02 +05:30
parent 39231d5ca4
commit 3ccb582b38
3 changed files with 275 additions and 50 deletions

View File

@@ -996,17 +996,15 @@ class _AgentDetailScreenState extends ConsumerState<AgentDetailScreen> {
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)

View File

@@ -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<String>.from((getValue() ?? <String>[]) 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: () => <String, dynamic>{},
);
return (match['label'] as String?) ?? val;
}
return GestureDetector(
onTap: () {
final newList = List<String>.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<String>.from(selected)
..remove(val);
setValue(newList);
},
child: const Icon(
Icons.close_rounded,
size: 16,
color: AppColors.accentOrange,
),
),
],
),
);
}).toList(),
),
],
],
);
}
Future<void> _openMultiSelectPicker({
required String title,
required List<Map<String, dynamic>> options,
required List<String> selected,
required void Function(dynamic) onChanged,
}) async {
final searchController = TextEditingController();
final workingSelected = List<String>.from(selected);
String query = '';
await showModalBottomSheet<void>(
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
// ---------------------------------------------------------------------------

View File

@@ -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