diff --git a/lib/controller/directory/add_contact_controller.dart b/lib/controller/directory/add_contact_controller.dart index d3fdb92..0c549c3 100644 --- a/lib/controller/directory/add_contact_controller.dart +++ b/lib/controller/directory/add_contact_controller.dart @@ -10,7 +10,7 @@ class AddContactController extends GetxController { final RxList tags = [].obs; final RxString selectedCategory = ''.obs; - final RxString selectedBucket = ''.obs; + final RxList selectedBuckets = [].obs; final RxString selectedProject = ''.obs; final RxList enteredTags = [].obs; @@ -50,7 +50,7 @@ class AddContactController extends GetxController { void resetForm() { selectedCategory.value = ''; selectedProject.value = ''; - selectedBucket.value = ''; + selectedBuckets.clear(); enteredTags.clear(); filteredSuggestions.clear(); filteredOrgSuggestions.clear(); @@ -100,7 +100,21 @@ class AddContactController extends GetxController { isSubmitting.value = true; final categoryId = categoriesMap[selectedCategory.value]; - final bucketId = bucketsMap[selectedBucket.value]; + final bucketIds = selectedBuckets + .map((name) => bucketsMap[name]) + .whereType() + .toList(); + + if (bucketIds.isEmpty) { + showAppSnackbar( + title: "Missing Buckets", + message: "Please select at least one bucket.", + type: SnackbarType.warning, + ); + isSubmitting.value = false; + return; + } + final projectIds = selectedProjects .map((name) => projectsMap[name]) .whereType() @@ -126,10 +140,10 @@ class AddContactController extends GetxController { return; } - if (selectedBucket.value.trim().isEmpty || bucketId == null) { + if (selectedBuckets.isEmpty) { showAppSnackbar( title: "Missing Bucket", - message: "Please select a bucket.", + message: "Please select at least one bucket.", type: SnackbarType.warning, ); isSubmitting.value = false; @@ -151,7 +165,7 @@ class AddContactController extends GetxController { if (selectedCategory.value.isNotEmpty && categoryId != null) "contactCategoryId": categoryId, if (projectIds.isNotEmpty) "projectIds": projectIds, - "bucketIds": [bucketId], + "bucketIds": bucketIds, if (enteredTags.isNotEmpty) "tags": tagObjects, if (emails.isNotEmpty) "contactEmails": emails, if (phones.isNotEmpty) "contactPhones": phones, diff --git a/lib/model/directory/add_contact_bottom_sheet.dart b/lib/model/directory/add_contact_bottom_sheet.dart index 924b09d..f0f456d 100644 --- a/lib/model/directory/add_contact_bottom_sheet.dart +++ b/lib/model/directory/add_contact_bottom_sheet.dart @@ -74,12 +74,20 @@ class _AddContactBottomSheetState extends State { ever(controller.isInitialized, (bool ready) { if (ready) { + // Buckets - map all + if (c.bucketIds.isNotEmpty) { + final names = c.bucketIds + .map((id) { + return controller.bucketsMap.entries + .firstWhereOrNull((e) => e.value == id) + ?.key; + }) + .whereType() + .toList(); + controller.selectedBuckets.assignAll(names); + } + // Projects and Category mapping - as before final projectIds = c.projectIds; - final bucketId = c.bucketIds.firstOrNull; - final category = c.contactCategory?.name; - - if (category != null) controller.selectedCategory.value = category; - if (projectIds != null) { controller.selectedProjects.assignAll( projectIds @@ -90,16 +98,12 @@ class _AddContactBottomSheetState extends State { .toList(), ); } - - if (bucketId != null) { - final name = controller.bucketsMap.entries - .firstWhereOrNull((e) => e.value == bucketId) - ?.key; - if (name != null) controller.selectedBucket.value = name; - } + final category = c.contactCategory?.name; + if (category != null) controller.selectedCategory.value = category; } }); } else { + showAdvanced.value = false; // Optional emailCtrls.add(TextEditingController()); emailLabels.add('Office'.obs); phoneCtrls.add(TextEditingController()); @@ -363,10 +367,129 @@ class _AddContactBottomSheetState extends State { ); } + Widget _bucketMultiSelectField() { + return _multiSelectField( + items: controller.buckets + .map((name) => FilterItem(id: name, name: name)) + .toList(), + fallback: "Choose Buckets", + selectedValues: controller.selectedBuckets, + ); + } + + Widget _multiSelectField({ + required List items, + required String fallback, + required RxList selectedValues, + }) { + if (items.isEmpty) return const SizedBox.shrink(); + + return Obx(() { + final selectedNames = items + .where((f) => selectedValues.contains(f.id)) + .map((f) => f.name) + .join(", "); + final displayText = selectedNames.isNotEmpty ? selectedNames : fallback; + + return Builder( + builder: (context) { + return GestureDetector( + onTap: () async { + final RenderBox button = context.findRenderObject() as RenderBox; + final RenderBox overlay = + Overlay.of(context).context.findRenderObject() as RenderBox; + final position = button.localToGlobal(Offset.zero); + + await showMenu( + context: context, + position: RelativeRect.fromLTRB( + position.dx, + position.dy + button.size.height, + overlay.size.width - position.dx - button.size.width, + 0, + ), + items: [ + PopupMenuItem( + enabled: false, + child: StatefulBuilder( + builder: (context, setState) { + return SizedBox( + width: 250, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: items.map((f) { + final isChecked = selectedValues.contains(f.id); + return CheckboxListTile( + dense: true, + title: Text(f.name), + value: isChecked, + contentPadding: EdgeInsets.zero, + controlAffinity: + ListTileControlAffinity.leading, + side: const BorderSide( + color: Colors.black, width: 1.5), + fillColor: + MaterialStateProperty.resolveWith( + (states) { + if (states + .contains(MaterialState.selected)) { + return Colors.indigo; // selected color + } + return Colors + .white; // unselected background + }), + checkColor: Colors.white, // tick color + onChanged: (val) { + if (val == true) { + selectedValues.add(f.id); + } else { + selectedValues.remove(f.id); + } + setState(() {}); + }, + ); + }).toList(), + ), + ), + ); + }, + ), + ), + ], + ); + }, + child: Container( + padding: MySpacing.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: MyText( + displayText, + style: const TextStyle(color: Colors.black87), + overflow: TextOverflow.ellipsis, + ), + ), + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], + ), + ), + ); + }, + ); + }); + } + void _handleSubmit() { bool valid = formKey.currentState?.validate() ?? false; - if (controller.selectedBucket.value.isEmpty) { + if (controller.selectedBuckets.isEmpty) { bucketError.value = "Bucket is required"; valid = false; } else { @@ -430,29 +553,14 @@ class _AddContactBottomSheetState extends State { MySpacing.height(16), _textField("Organization", orgCtrl, required: true), MySpacing.height(16), - _labelWithStar("Bucket", required: true), + _labelWithStar("Buckets", required: true), MySpacing.height(8), Stack( children: [ - _popupSelector(controller.selectedBucket, controller.buckets, - "Choose Bucket"), - Positioned( - left: 0, - right: 0, - top: 56, - child: Obx(() => bucketError.value.isEmpty - ? const SizedBox.shrink() - : Padding( - padding: - const EdgeInsets.symmetric(horizontal: 8.0), - child: Text(bucketError.value, - style: const TextStyle( - color: Colors.red, fontSize: 12)), - )), - ), + _bucketMultiSelectField(), ], ), - MySpacing.height(24), + MySpacing.height(12), Obx(() => GestureDetector( onTap: () => showAdvanced.toggle(), child: Row( @@ -562,3 +670,9 @@ class _AddContactBottomSheetState extends State { }); } } + +class FilterItem { + final String id; + final String name; + FilterItem({required this.id, required this.name}); +}