feat(contact): streamline validation logic and enhance UI for adding contacts

This commit is contained in:
Vaibhav Surve 2025-07-14 10:03:27 +05:30
parent 33d267f18e
commit 574e7df447
2 changed files with 204 additions and 249 deletions

View File

@ -101,7 +101,7 @@ class AddContactController extends GetxController {
.whereType<String>() .whereType<String>()
.toList(); .toList();
// === Per-field Validation with Specific Messages === // === Required validations only for name, organization, and bucket ===
if (name.trim().isEmpty) { if (name.trim().isEmpty) {
showAppSnackbar( showAppSnackbar(
title: "Missing Name", title: "Missing Name",
@ -120,51 +120,6 @@ class AddContactController extends GetxController {
return; return;
} }
if (emails.isEmpty) {
showAppSnackbar(
title: "Missing Email",
message: "Please add at least one email.",
type: SnackbarType.warning,
);
return;
}
if (phones.isEmpty) {
showAppSnackbar(
title: "Missing Phone Number",
message: "Please add at least one phone number.",
type: SnackbarType.warning,
);
return;
}
if (address.trim().isEmpty) {
showAppSnackbar(
title: "Missing Address",
message: "Please enter the address.",
type: SnackbarType.warning,
);
return;
}
if (description.trim().isEmpty) {
showAppSnackbar(
title: "Missing Description",
message: "Please enter a description.",
type: SnackbarType.warning,
);
return;
}
if (selectedCategory.value.trim().isEmpty || categoryId == null) {
showAppSnackbar(
title: "Missing Category",
message: "Please select a contact category.",
type: SnackbarType.warning,
);
return;
}
if (selectedBucket.value.trim().isEmpty || bucketId == null) { if (selectedBucket.value.trim().isEmpty || bucketId == null) {
showAppSnackbar( showAppSnackbar(
title: "Missing Bucket", title: "Missing Bucket",
@ -174,25 +129,7 @@ class AddContactController extends GetxController {
return; return;
} }
if (selectedProjects.isEmpty || projectIds.isEmpty) { // === Build body (include optional fields if available) ===
showAppSnackbar(
title: "Missing Projects",
message: "Please select at least one project.",
type: SnackbarType.warning,
);
return;
}
if (enteredTags.isEmpty) {
showAppSnackbar(
title: "Missing Tags",
message: "Please enter at least one tag.",
type: SnackbarType.warning,
);
return;
}
// === Submit if all validations passed ===
try { try {
final tagObjects = enteredTags.map((tagName) { final tagObjects = enteredTags.map((tagName) {
final tagId = tagsMap[tagName]; final tagId = tagsMap[tagName];
@ -205,14 +142,15 @@ class AddContactController extends GetxController {
if (id != null) "id": id, if (id != null) "id": id,
"name": name.trim(), "name": name.trim(),
"organization": organization.trim(), "organization": organization.trim(),
if (selectedCategory.value.isNotEmpty && categoryId != null)
"contactCategoryId": categoryId, "contactCategoryId": categoryId,
"projectIds": projectIds, if (projectIds.isNotEmpty) "projectIds": projectIds,
"bucketIds": [bucketId], "bucketIds": [bucketId],
"tags": tagObjects, if (enteredTags.isNotEmpty) "tags": tagObjects,
"contactEmails": emails, if (emails.isNotEmpty) "contactEmails": emails,
"contactPhones": phones, if (phones.isNotEmpty) "contactPhones": phones,
"address": address.trim(), if (address.trim().isNotEmpty) "address": address.trim(),
"description": description.trim(), if (description.trim().isNotEmpty) "description": description.trim(),
}; };
logSafe("${id != null ? 'Updating' : 'Creating'} contact"); logSafe("${id != null ? 'Updating' : 'Creating'} contact");

View File

@ -25,7 +25,7 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
final addressController = TextEditingController(); final addressController = TextEditingController();
final descriptionController = TextEditingController(); final descriptionController = TextEditingController();
final tagTextController = TextEditingController(); final tagTextController = TextEditingController();
final RxBool showAdvanced = false.obs;
final RxList<TextEditingController> emailControllers = final RxList<TextEditingController> emailControllers =
<TextEditingController>[].obs; <TextEditingController>[].obs;
final RxList<RxString> emailLabels = <RxString>[].obs; final RxList<RxString> emailLabels = <RxString>[].obs;
@ -529,15 +529,43 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
), ),
), ),
MySpacing.height(24), MySpacing.height(24),
_sectionLabel("Basic Info"), _sectionLabel("Required Fields"),
MySpacing.height(16), MySpacing.height(12),
_buildTextField("Name", nameController), _buildTextField("Name", nameController),
MySpacing.height(16), MySpacing.height(16),
_buildOrganizationField(), _buildOrganizationField(),
MySpacing.height(16),
MyText.labelMedium("Select Bucket"),
MySpacing.height(8),
_popupSelector(
hint: "Select Bucket",
selectedValue: controller.selectedBucket,
options: controller.buckets,
),
MySpacing.height(24),
// Toggle for Advanced Section
Obx(() => GestureDetector(
onTap: () => showAdvanced.toggle(),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.labelLarge("Advanced Details (Optional)",
fontWeight: 600),
Icon(showAdvanced.value
? Icons.expand_less
: Icons.expand_more),
],
),
)),
Obx(() => showAdvanced.value
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MySpacing.height(24), MySpacing.height(24),
_sectionLabel("Contact Info"), _sectionLabel("Contact Info"),
MySpacing.height(16), MySpacing.height(16),
Obx(() => _buildEmailList()), _buildEmailList(),
TextButton.icon( TextButton.icon(
onPressed: () { onPressed: () {
emailControllers.add(TextEditingController()); emailControllers.add(TextEditingController());
@ -546,7 +574,7 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
label: const Text("Add Email"), label: const Text("Add Email"),
), ),
Obx(() => _buildPhoneList()), _buildPhoneList(),
TextButton.icon( TextButton.icon(
onPressed: () { onPressed: () {
phoneControllers.add(TextEditingController()); phoneControllers.add(TextEditingController());
@ -568,7 +596,34 @@ 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),
GestureDetector( _projectSelectorUI(),
MySpacing.height(16),
MyText.labelMedium("Tags"),
MySpacing.height(8),
_tagInputSection(),
MySpacing.height(16),
_buildTextField("Address", addressController,
maxLines: 2),
MySpacing.height(16),
_buildTextField(
"Description", descriptionController,
maxLines: 2),
],
)
: SizedBox()),
MySpacing.height(24),
_buildActionButtons(),
],
),
)),
),
);
});
}
Widget _projectSelectorUI() {
return GestureDetector(
onTap: () async { onTap: () async {
await showDialog( await showDialog(
context: context, context: context,
@ -580,33 +635,25 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
width: double.maxFinite, width: double.maxFinite,
child: ListView( child: ListView(
shrinkWrap: true, shrinkWrap: true,
children: children: controller.globalProjects.map((project) {
controller.globalProjects.map((project) { final isSelected =
final isSelected = controller controller.selectedProjects.contains(project);
.selectedProjects
.contains(project);
return Theme( return Theme(
data: Theme.of(context).copyWith( data: Theme.of(context).copyWith(
unselectedWidgetColor: Colors unselectedWidgetColor: Colors.black,
.black, // checkbox border when not selected
checkboxTheme: CheckboxThemeData( checkboxTheme: CheckboxThemeData(
fillColor: MaterialStateProperty fillColor: MaterialStateProperty.resolveWith<Color>(
.resolveWith<Color>((states) { (states) {
if (states.contains( if (states.contains(MaterialState.selected)) {
MaterialState.selected)) { return Colors.white;
return Colors
.white; // fill when selected
} }
return Colors.transparent; return Colors.transparent;
}), }),
checkColor: MaterialStateProperty.all( checkColor: MaterialStateProperty.all(Colors.black),
Colors.black), // check mark color side:
side: const BorderSide( const BorderSide(color: Colors.black, width: 2),
color: Colors.black,
width: 2), // border color
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: borderRadius: BorderRadius.circular(4),
BorderRadius.circular(4),
), ),
), ),
), ),
@ -616,11 +663,9 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
value: isSelected, value: isSelected,
onChanged: (bool? selected) { onChanged: (bool? selected) {
if (selected == true) { if (selected == true) {
controller.selectedProjects controller.selectedProjects.add(project);
.add(project);
} else { } else {
controller.selectedProjects controller.selectedProjects.remove(project);
.remove(project);
} }
}, },
), ),
@ -655,9 +700,7 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
selected.isEmpty selected.isEmpty ? "Select Projects" : selected.join(', '),
? "Select Projects"
: selected.join(', '),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 14), style: const TextStyle(fontSize: 14),
), ),
@ -667,32 +710,6 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
); );
}), }),
), ),
),
MySpacing.height(16),
MyText.labelMedium("Select Bucket"),
MySpacing.height(8),
_popupSelector(
hint: "Select Bucket",
selectedValue: controller.selectedBucket,
options: controller.buckets,
),
MySpacing.height(16),
MyText.labelMedium("Tags"),
MySpacing.height(8),
_tagInputSection(),
MySpacing.height(16),
_buildTextField("Address", addressController, maxLines: 2),
MySpacing.height(16),
_buildTextField("Description", descriptionController,
maxLines: 2),
MySpacing.height(24),
_buildActionButtons(),
],
),
),
),
),
); );
});
} }
} }