feat(contact): support multiple project selection in AddContact functionality
This commit is contained in:
parent
a8c890a60d
commit
2fef2e508e
@ -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,
|
||||||
|
@ -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"),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user