added multiple bucket assignment

This commit is contained in:
Vaibhav Surve 2025-10-10 19:46:17 +05:30
parent 5dc2db0a8b
commit cd92d4d309
2 changed files with 165 additions and 37 deletions

View File

@ -10,7 +10,7 @@ class AddContactController extends GetxController {
final RxList<String> tags = <String>[].obs;
final RxString selectedCategory = ''.obs;
final RxString selectedBucket = ''.obs;
final RxList<String> selectedBuckets = <String>[].obs;
final RxString selectedProject = ''.obs;
final RxList<String> enteredTags = <String>[].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<String>()
.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<String>()
@ -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,

View File

@ -74,12 +74,20 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
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<String>()
.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<AddContactBottomSheet> {
.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<AddContactBottomSheet> {
);
}
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<FilterItem> items,
required String fallback,
required RxList<String> 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<Color>(
(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<AddContactBottomSheet> {
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<AddContactBottomSheet> {
});
}
}
class FilterItem {
final String id;
final String name;
FilterItem({required this.id, required this.name});
}