feat(contact): support multiple project selection in AddContact functionality

This commit is contained in:
Vaibhav Surve 2025-07-08 15:28:20 +05:30
parent a8c890a60d
commit 2fef2e508e
2 changed files with 131 additions and 23 deletions

View File

@ -25,6 +25,7 @@ class AddContactController extends GetxController {
final RxMap<String, String> projectsMap = <String, String>{}.obs; final RxMap<String, String> projectsMap = <String, String>{}.obs;
final RxMap<String, String> tagsMap = <String, String>{}.obs; final RxMap<String, String> tagsMap = <String, String>{}.obs;
final RxBool isInitialized = false.obs; final RxBool isInitialized = false.obs;
final RxList<String> selectedProjects = <String>[].obs;
@override @override
void onInit() { void onInit() {
@ -54,6 +55,7 @@ class AddContactController extends GetxController {
enteredTags.clear(); enteredTags.clear();
filteredSuggestions.clear(); filteredSuggestions.clear();
filteredOrgSuggestions.clear(); filteredOrgSuggestions.clear();
selectedProjects.clear();
} }
Future<void> fetchBuckets() async { Future<void> fetchBuckets() async {
@ -96,7 +98,10 @@ class AddContactController extends GetxController {
}) async { }) async {
final categoryId = categoriesMap[selectedCategory.value]; final categoryId = categoriesMap[selectedCategory.value];
final bucketId = bucketsMap[selectedBucket.value]; final bucketId = bucketsMap[selectedBucket.value];
final projectId = projectsMap[selectedProject.value]; final projectIds = selectedProjects
.map((name) => projectsMap[name])
.whereType<String>()
.toList();
// === Per-field Validation with Specific Messages === // === Per-field Validation with Specific Messages ===
if (name.trim().isEmpty) { if (name.trim().isEmpty) {
@ -171,10 +176,10 @@ class AddContactController extends GetxController {
return; return;
} }
if (selectedProject.value.trim().isEmpty || projectId == null) { if (selectedProjects.isEmpty || projectIds.isEmpty) {
showAppSnackbar( showAppSnackbar(
title: "Missing Project", title: "Missing Projects",
message: "Please select a project.", message: "Please select at least one project.",
type: SnackbarType.warning, type: SnackbarType.warning,
); );
return; return;
@ -203,7 +208,7 @@ class AddContactController extends GetxController {
"name": name.trim(), "name": name.trim(),
"organization": organization.trim(), "organization": organization.trim(),
"contactCategoryId": categoryId, "contactCategoryId": categoryId,
"projectIds": [projectId], "projectIds": projectIds,
"bucketIds": [bucketId], "bucketIds": [bucketId],
"tags": tagObjects, "tags": tagObjects,
"contactEmails": emails, "contactEmails": emails,

View File

@ -74,7 +74,7 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
ever(controller.isInitialized, (bool ready) { ever(controller.isInitialized, (bool ready) {
if (ready) { if (ready) {
final projectId = widget.existingContact!.projectIds?.firstOrNull; final projectIds = widget.existingContact!.projectIds;
final bucketId = widget.existingContact!.bucketIds.firstOrNull; final bucketId = widget.existingContact!.bucketIds.firstOrNull;
final categoryName = widget.existingContact!.contactCategory?.name; final categoryName = widget.existingContact!.contactCategory?.name;
@ -82,15 +82,17 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
controller.selectedCategory.value = categoryName; controller.selectedCategory.value = categoryName;
} }
if (projectId != null) { if (projectIds != null) {
final name = controller.projectsMap.entries final names = projectIds
.firstWhereOrNull((e) => e.value == projectId) .map((id) {
?.key; return controller.projectsMap.entries
if (name != null) { .firstWhereOrNull((e) => e.value == id)
controller.selectedProject.value = name; ?.key;
} })
.whereType<String>()
.toList();
controller.selectedProjects.assignAll(names);
} }
if (bucketId != null) { if (bucketId != null) {
final name = controller.bucketsMap.entries final name = controller.bucketsMap.entries
.firstWhereOrNull((e) => e.value == bucketId) .firstWhereOrNull((e) => e.value == bucketId)
@ -270,12 +272,18 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
onTap: () async { onTap: () async {
final selected = await showMenu<String>( final selected = await showMenu<String>(
context: context, context: context,
position: const RelativeRect.fromLTRB(100, 300, 100, 0), position: RelativeRect.fromLTRB(100, 300, 100, 0),
items: options items: options.map((option) {
.map((e) => PopupMenuItem(value: e, child: Text(e))) return PopupMenuItem<String>(
.toList(), value: option,
child: Text(option),
);
}).toList(),
); );
if (selected != null) selectedValue.value = selected;
if (selected != null) {
selectedValue.value = selected;
}
}, },
child: Container( child: Container(
height: 48, height: 48,
@ -560,10 +568,105 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
MySpacing.height(16), MySpacing.height(16),
MyText.labelMedium("Select Projects"), MyText.labelMedium("Select Projects"),
MySpacing.height(8), MySpacing.height(8),
_popupSelector( GestureDetector(
hint: "Select Project", onTap: () async {
selectedValue: controller.selectedProject, await showDialog(
options: controller.globalProjects, 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<Color>((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), MySpacing.height(16),
MyText.labelMedium("Select Bucket"), MyText.labelMedium("Select Bucket"),