From 574e7df447cd59424c524856a3e98733ac9f448d Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 14 Jul 2025 10:03:27 +0530 Subject: [PATCH] feat(contact): streamline validation logic and enhance UI for adding contacts --- .../directory/add_contact_controller.dart | 84 +--- .../directory/add_contact_bottom_sheet.dart | 369 +++++++++--------- 2 files changed, 204 insertions(+), 249 deletions(-) diff --git a/lib/controller/directory/add_contact_controller.dart b/lib/controller/directory/add_contact_controller.dart index 463d2e8..3cfcc57 100644 --- a/lib/controller/directory/add_contact_controller.dart +++ b/lib/controller/directory/add_contact_controller.dart @@ -73,7 +73,7 @@ class AddContactController extends GetxController { } catch (e) { logSafe("Failed to fetch buckets: \$e", level: LogLevel.error); } - } + } Future fetchOrganizationNames() async { try { @@ -101,7 +101,7 @@ class AddContactController extends GetxController { .whereType() .toList(); - // === Per-field Validation with Specific Messages === + // === Required validations only for name, organization, and bucket === if (name.trim().isEmpty) { showAppSnackbar( title: "Missing Name", @@ -120,51 +120,6 @@ class AddContactController extends GetxController { 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) { showAppSnackbar( title: "Missing Bucket", @@ -174,25 +129,7 @@ class AddContactController extends GetxController { return; } - if (selectedProjects.isEmpty || projectIds.isEmpty) { - 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 === + // === Build body (include optional fields if available) === try { final tagObjects = enteredTags.map((tagName) { final tagId = tagsMap[tagName]; @@ -205,14 +142,15 @@ class AddContactController extends GetxController { if (id != null) "id": id, "name": name.trim(), "organization": organization.trim(), - "contactCategoryId": categoryId, - "projectIds": projectIds, + if (selectedCategory.value.isNotEmpty && categoryId != null) + "contactCategoryId": categoryId, + if (projectIds.isNotEmpty) "projectIds": projectIds, "bucketIds": [bucketId], - "tags": tagObjects, - "contactEmails": emails, - "contactPhones": phones, - "address": address.trim(), - "description": description.trim(), + if (enteredTags.isNotEmpty) "tags": tagObjects, + if (emails.isNotEmpty) "contactEmails": emails, + if (phones.isNotEmpty) "contactPhones": phones, + if (address.trim().isNotEmpty) "address": address.trim(), + if (description.trim().isNotEmpty) "description": description.trim(), }; logSafe("${id != null ? 'Updating' : 'Creating'} contact"); diff --git a/lib/model/directory/add_contact_bottom_sheet.dart b/lib/model/directory/add_contact_bottom_sheet.dart index 97d2e95..77f9bce 100644 --- a/lib/model/directory/add_contact_bottom_sheet.dart +++ b/lib/model/directory/add_contact_bottom_sheet.dart @@ -25,7 +25,7 @@ class _AddContactBottomSheetState extends State { final addressController = TextEditingController(); final descriptionController = TextEditingController(); final tagTextController = TextEditingController(); - + final RxBool showAdvanced = false.obs; final RxList emailControllers = [].obs; final RxList emailLabels = [].obs; @@ -514,185 +514,202 @@ class _AddContactBottomSheetState extends State { borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), ), child: Padding( - padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), - child: Form( - key: formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: MyText.titleMedium( - widget.existingContact != null - ? "Edit Contact" - : "Create New Contact", - fontWeight: 700, - ), - ), - MySpacing.height(24), - _sectionLabel("Basic Info"), - MySpacing.height(16), - _buildTextField("Name", nameController), - MySpacing.height(16), - _buildOrganizationField(), - MySpacing.height(24), - _sectionLabel("Contact Info"), - MySpacing.height(16), - Obx(() => _buildEmailList()), - TextButton.icon( - onPressed: () { - emailControllers.add(TextEditingController()); - emailLabels.add('Office'.obs); - }, - icon: const Icon(Icons.add), - label: const Text("Add Email"), - ), - Obx(() => _buildPhoneList()), - TextButton.icon( - onPressed: () { - phoneControllers.add(TextEditingController()); - phoneLabels.add('Work'.obs); - }, - icon: const Icon(Icons.add), - label: const Text("Add Phone"), - ), - MySpacing.height(24), - _sectionLabel("Other Details"), - MySpacing.height(16), - MyText.labelMedium("Category"), - MySpacing.height(8), - _popupSelector( - hint: "Select Category", - selectedValue: controller.selectedCategory, - options: controller.categories, - ), - MySpacing.height(16), - MyText.labelMedium("Select Projects"), - MySpacing.height(8), - 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), + padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), + child: Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: MyText.titleMedium( + widget.existingContact != null + ? "Edit Contact" + : "Create New Contact", + fontWeight: 700, ), - 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"), - 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(), - ], - ), - ), - ), + MySpacing.height(24), + _sectionLabel("Required Fields"), + MySpacing.height(12), + _buildTextField("Name", nameController), + MySpacing.height(16), + _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), + _sectionLabel("Contact Info"), + MySpacing.height(16), + _buildEmailList(), + TextButton.icon( + onPressed: () { + emailControllers.add(TextEditingController()); + emailLabels.add('Office'.obs); + }, + icon: const Icon(Icons.add), + label: const Text("Add Email"), + ), + _buildPhoneList(), + TextButton.icon( + onPressed: () { + phoneControllers.add(TextEditingController()); + phoneLabels.add('Work'.obs); + }, + icon: const Icon(Icons.add), + label: const Text("Add Phone"), + ), + MySpacing.height(24), + _sectionLabel("Other Details"), + MySpacing.height(16), + MyText.labelMedium("Category"), + MySpacing.height(8), + _popupSelector( + hint: "Select Category", + selectedValue: controller.selectedCategory, + options: controller.categories, + ), + MySpacing.height(16), + MyText.labelMedium("Select Projects"), + MySpacing.height(8), + _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 { + 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, + checkboxTheme: CheckboxThemeData( + fillColor: MaterialStateProperty.resolveWith( + (states) { + if (states.contains(MaterialState.selected)) { + return Colors.white; + } + return Colors.transparent; + }), + checkColor: MaterialStateProperty.all(Colors.black), + side: + const BorderSide(color: Colors.black, width: 2), + 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), + ], + ); + }), + ), + ); + } }