From 2fef2e508eff5c79f607065ffc37aed1a67a7ccb Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Tue, 8 Jul 2025 15:28:20 +0530 Subject: [PATCH] feat(contact): support multiple project selection in AddContact functionality --- .../directory/add_contact_controller.dart | 15 +- .../directory/add_contact_bottom_sheet.dart | 139 +++++++++++++++--- 2 files changed, 131 insertions(+), 23 deletions(-) diff --git a/lib/controller/directory/add_contact_controller.dart b/lib/controller/directory/add_contact_controller.dart index 63c29bd..bcc1abc 100644 --- a/lib/controller/directory/add_contact_controller.dart +++ b/lib/controller/directory/add_contact_controller.dart @@ -25,6 +25,7 @@ class AddContactController extends GetxController { final RxMap projectsMap = {}.obs; final RxMap tagsMap = {}.obs; final RxBool isInitialized = false.obs; + final RxList selectedProjects = [].obs; @override void onInit() { @@ -54,6 +55,7 @@ class AddContactController extends GetxController { enteredTags.clear(); filteredSuggestions.clear(); filteredOrgSuggestions.clear(); + selectedProjects.clear(); } Future fetchBuckets() async { @@ -96,7 +98,10 @@ class AddContactController extends GetxController { }) async { final categoryId = categoriesMap[selectedCategory.value]; final bucketId = bucketsMap[selectedBucket.value]; - final projectId = projectsMap[selectedProject.value]; + final projectIds = selectedProjects + .map((name) => projectsMap[name]) + .whereType() + .toList(); // === Per-field Validation with Specific Messages === if (name.trim().isEmpty) { @@ -171,10 +176,10 @@ class AddContactController extends GetxController { return; } - if (selectedProject.value.trim().isEmpty || projectId == null) { + if (selectedProjects.isEmpty || projectIds.isEmpty) { showAppSnackbar( - title: "Missing Project", - message: "Please select a project.", + title: "Missing Projects", + message: "Please select at least one project.", type: SnackbarType.warning, ); return; @@ -203,7 +208,7 @@ class AddContactController extends GetxController { "name": name.trim(), "organization": organization.trim(), "contactCategoryId": categoryId, - "projectIds": [projectId], + "projectIds": projectIds, "bucketIds": [bucketId], "tags": tagObjects, "contactEmails": emails, diff --git a/lib/model/directory/add_contact_bottom_sheet.dart b/lib/model/directory/add_contact_bottom_sheet.dart index c509df4..97d2e95 100644 --- a/lib/model/directory/add_contact_bottom_sheet.dart +++ b/lib/model/directory/add_contact_bottom_sheet.dart @@ -74,7 +74,7 @@ class _AddContactBottomSheetState extends State { ever(controller.isInitialized, (bool ready) { if (ready) { - final projectId = widget.existingContact!.projectIds?.firstOrNull; + final projectIds = widget.existingContact!.projectIds; final bucketId = widget.existingContact!.bucketIds.firstOrNull; final categoryName = widget.existingContact!.contactCategory?.name; @@ -82,15 +82,17 @@ class _AddContactBottomSheetState extends State { controller.selectedCategory.value = categoryName; } - if (projectId != null) { - final name = controller.projectsMap.entries - .firstWhereOrNull((e) => e.value == projectId) - ?.key; - if (name != null) { - controller.selectedProject.value = name; - } + if (projectIds != null) { + final names = projectIds + .map((id) { + return controller.projectsMap.entries + .firstWhereOrNull((e) => e.value == id) + ?.key; + }) + .whereType() + .toList(); + controller.selectedProjects.assignAll(names); } - if (bucketId != null) { final name = controller.bucketsMap.entries .firstWhereOrNull((e) => e.value == bucketId) @@ -270,12 +272,18 @@ class _AddContactBottomSheetState extends State { onTap: () async { final selected = await showMenu( context: context, - position: const RelativeRect.fromLTRB(100, 300, 100, 0), - items: options - .map((e) => PopupMenuItem(value: e, child: Text(e))) - .toList(), + position: RelativeRect.fromLTRB(100, 300, 100, 0), + items: options.map((option) { + return PopupMenuItem( + value: option, + child: Text(option), + ); + }).toList(), ); - if (selected != null) selectedValue.value = selected; + + if (selected != null) { + selectedValue.value = selected; + } }, child: Container( height: 48, @@ -560,10 +568,105 @@ class _AddContactBottomSheetState extends State { MySpacing.height(16), MyText.labelMedium("Select Projects"), MySpacing.height(8), - _popupSelector( - hint: "Select Project", - selectedValue: controller.selectedProject, - options: controller.globalProjects, + GestureDetector( + onTap: () async { + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Select Projects'), + content: Obx(() { + return SizedBox( + width: double.maxFinite, + child: ListView( + shrinkWrap: true, + children: + controller.globalProjects.map((project) { + final isSelected = controller + .selectedProjects + .contains(project); + return Theme( + data: Theme.of(context).copyWith( + unselectedWidgetColor: Colors + .black, // checkbox border when not selected + checkboxTheme: CheckboxThemeData( + fillColor: MaterialStateProperty + .resolveWith((states) { + if (states.contains( + MaterialState.selected)) { + return Colors + .white; // fill when selected + } + return Colors.transparent; + }), + checkColor: MaterialStateProperty.all( + Colors.black), // check mark color + side: const BorderSide( + color: Colors.black, + width: 2), // border color + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(4), + ), + ), + ), + child: CheckboxListTile( + dense: true, + title: Text(project), + value: isSelected, + onChanged: (bool? selected) { + if (selected == true) { + controller.selectedProjects + .add(project); + } else { + controller.selectedProjects + .remove(project); + } + }, + ), + ); + }).toList(), + ), + ); + }), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Done'), + ), + ], + ); + }, + ); + }, + child: Container( + height: 48, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + alignment: Alignment.centerLeft, + child: Obx(() { + final selected = controller.selectedProjects; + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + selected.isEmpty + ? "Select Projects" + : selected.join(', '), + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ), + ), + const Icon(Icons.expand_more, size: 20), + ], + ); + }), + ), ), MySpacing.height(16), MyText.labelMedium("Select Bucket"),