From ddbc1ec1e52ebeecca69fea2b816c4ede9a44e6d Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Tue, 29 Jul 2025 18:05:37 +0530 Subject: [PATCH] Refactor ContactDetailScreen and DirectoryView for improved readability and performance - Moved the Delta to HTML conversion logic outside of the ContactDetailScreen class for better separation of concerns. - Simplified the handling of email and phone display in DirectoryView to show only the first entry, reducing redundancy. - Enhanced the layout and structure of the ContactDetailScreen for better maintainability. - Introduced a new ProjectLabel widget to encapsulate project display logic in the ContactDetailScreen. - Cleaned up unnecessary comments and improved code formatting for consistency. --- .../directory/add_contact_bottom_sheet.dart | 814 +++++++++--------- lib/view/directory/contact_detail_screen.dart | 503 +++++------ lib/view/directory/directory_view.dart | 130 +-- 3 files changed, 705 insertions(+), 742 deletions(-) diff --git a/lib/model/directory/add_contact_bottom_sheet.dart b/lib/model/directory/add_contact_bottom_sheet.dart index 31e0f4a..bb8761a 100644 --- a/lib/model/directory/add_contact_bottom_sheet.dart +++ b/lib/model/directory/add_contact_bottom_sheet.dart @@ -18,89 +18,75 @@ class AddContactBottomSheet extends StatefulWidget { } class _AddContactBottomSheetState extends State { - final controller = Get.put(AddContactController()); + // Controllers and state + final AddContactController controller = Get.put(AddContactController()); final formKey = GlobalKey(); - final nameController = TextEditingController(); final orgController = TextEditingController(); final addressController = TextEditingController(); final descriptionController = TextEditingController(); final tagTextController = TextEditingController(); - final RxBool showAdvanced = false.obs; - final RxList emailControllers = - [].obs; - final RxList emailLabels = [].obs; - final RxList phoneControllers = - [].obs; - final RxList phoneLabels = [].obs; + // Use Rx for advanced toggle and dynamic fields + final showAdvanced = false.obs; + final emailControllers = [].obs; + final emailLabels = [].obs; + final phoneControllers = [].obs; + final phoneLabels = [].obs; + + // For required bucket validation (new) + final bucketError = ''.obs; @override void initState() { super.initState(); controller.resetForm(); + _initFields(); + } - nameController.text = widget.existingContact?.name ?? ''; - orgController.text = widget.existingContact?.organization ?? ''; - addressController.text = widget.existingContact?.address ?? ''; - descriptionController.text = widget.existingContact?.description ?? ''; - tagTextController.clear(); - - if (widget.existingContact != null) { - emailControllers.clear(); - emailLabels.clear(); - for (var email in widget.existingContact!.contactEmails) { - emailControllers.add(TextEditingController(text: email.emailAddress)); - emailLabels.add((email.label).obs); - } - if (emailControllers.isEmpty) { - emailControllers.add(TextEditingController()); - emailLabels.add('Office'.obs); - } - - phoneControllers.clear(); - phoneLabels.clear(); - for (var phone in widget.existingContact!.contactPhones) { - phoneControllers.add(TextEditingController(text: phone.phoneNumber)); - phoneLabels.add((phone.label).obs); - } - if (phoneControllers.isEmpty) { - phoneControllers.add(TextEditingController()); - phoneLabels.add('Work'.obs); - } - - controller.enteredTags.assignAll( - widget.existingContact!.tags.map((tag) => tag.name).toList(), - ); - + void _initFields() { + final c = widget.existingContact; + if (c != null) { + nameController.text = c.name; + orgController.text = c.organization; + addressController.text = c.address; + descriptionController.text = c.description ; + } + if (c != null) { + emailControllers.assignAll(c.contactEmails.isEmpty + ? [TextEditingController()] + : c.contactEmails.map((e) => TextEditingController(text: e.emailAddress))); + emailLabels.assignAll(c.contactEmails.isEmpty + ? ['Office'.obs] + : c.contactEmails.map((e) => e.label.obs)); + phoneControllers.assignAll(c.contactPhones.isEmpty + ? [TextEditingController()] + : c.contactPhones.map((p) => TextEditingController(text: p.phoneNumber))); + phoneLabels.assignAll(c.contactPhones.isEmpty + ? ['Work'.obs] + : c.contactPhones.map((p) => p.label.obs)); + controller.enteredTags.assignAll(c.tags.map((tag) => tag.name)); ever(controller.isInitialized, (bool ready) { if (ready) { - final projectIds = widget.existingContact!.projectIds; - final bucketId = widget.existingContact!.bucketIds.firstOrNull; - final categoryName = widget.existingContact!.contactCategory?.name; - - if (categoryName != null) { - controller.selectedCategory.value = categoryName; - } - + final projectIds = c.projectIds; + final bucketId = c.bucketIds.firstOrNull; + final categoryName = c.contactCategory?.name; + if (categoryName != null) controller.selectedCategory.value = categoryName; if (projectIds != null) { - final names = projectIds - .map((id) { - return controller.projectsMap.entries + controller.selectedProjects.assignAll( + projectIds // + .map((id) => controller.projectsMap.entries .firstWhereOrNull((e) => e.value == id) - ?.key; - }) - .whereType() - .toList(); - controller.selectedProjects.assignAll(names); + ?.key) + .whereType() + .toList(), + ); } if (bucketId != null) { final name = controller.bucketsMap.entries .firstWhereOrNull((e) => e.value == bucketId) ?.key; - if (name != null) { - controller.selectedBucket.value = name; - } + if (name != null) controller.selectedBucket.value = name; } } }); @@ -110,6 +96,7 @@ class _AddContactBottomSheetState extends State { phoneControllers.add(TextEditingController()); phoneLabels.add('Work'.obs); } + tagTextController.clear(); } @override @@ -119,12 +106,17 @@ class _AddContactBottomSheetState extends State { tagTextController.dispose(); addressController.dispose(); descriptionController.dispose(); - emailControllers.forEach((e) => e.dispose()); - phoneControllers.forEach((p) => p.dispose()); + for (final c in emailControllers) { + c.dispose(); + } + for (final c in phoneControllers) { + c.dispose(); + } Get.delete(); super.dispose(); } + // --- COMMON WIDGETS --- InputDecoration _inputDecoration(String hint) => InputDecoration( hintText: hint, hintStyle: MyTextStyle.bodySmall(xMuted: true), @@ -142,19 +134,21 @@ class _AddContactBottomSheetState extends State { borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Colors.blueAccent, width: 1.5), ), - contentPadding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), isDense: true, ); - Widget _buildLabeledRow( - String label, - RxString selectedLabel, - List options, - String inputLabel, - TextEditingController controller, - TextInputType inputType, - {VoidCallback? onRemove}) { + // DRY'd: LABELED FIELD ROW (used for phone/email) + Widget _buildLabeledRow({ + required String label, + required RxString selectedLabel, + required List options, + required String inputLabel, + required TextEditingController controller, + required TextInputType inputType, + VoidCallback? onRemove, + Widget? suffixIcon, + }) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -165,9 +159,10 @@ class _AddContactBottomSheetState extends State { MyText.labelMedium(label), MySpacing.height(8), _popupSelector( - hint: "Label", - selectedValue: selectedLabel, - options: options), + hint: "Label", + selectedValue: selectedLabel, + options: options, + ), ], ), ), @@ -187,33 +182,17 @@ class _AddContactBottomSheetState extends State { : [], decoration: _inputDecoration("Enter $inputLabel").copyWith( counterText: "", - suffixIcon: inputType == TextInputType.phone - ? IconButton( - icon: const Icon(Icons.contact_phone, - color: Colors.blue), - onPressed: () async { - final selectedPhone = - await ContactPickerHelper.pickIndianPhoneNumber( - context); - if (selectedPhone != null) { - controller.text = selectedPhone; - } - }, - ) - : null, + suffixIcon: suffixIcon, ), validator: (value) { - if (value == null || value.trim().isEmpty) - return "$inputLabel is required"; + if (value == null || value.trim().isEmpty) return null; final trimmed = value.trim(); - if (inputType == TextInputType.phone) { - if (!RegExp(r'^[1-9][0-9]{9}$').hasMatch(trimmed)) { - return "Enter valid phone number"; - } + if (inputType == TextInputType.phone && + !RegExp(r'^[1-9][0-9]{9}$').hasMatch(trimmed)) { + return "Enter valid phone number"; } if (inputType == TextInputType.emailAddress && - !RegExp(r'^[\w.-]+@([\w-]+\.)+[\w-]{2,4}$') - .hasMatch(trimmed)) { + !RegExp(r'^[\w.-]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(trimmed)) { return "Enter valid email"; } return null; @@ -234,94 +213,110 @@ class _AddContactBottomSheetState extends State { ); } - Widget _buildEmailList() => Column( - children: List.generate(emailControllers.length, (index) { + // DRY: List builder for email/phone fields + Widget _buildDynamicList({ + required RxList ctrls, + required RxList labels, + required List labelOptions, + required String label, + required String inputLabel, + required TextInputType inputType, + required RxList listToRemoveFrom, + Widget? phoneSuffixIcon, + }) { + return Obx(() { + return Column( + children: List.generate(ctrls.length, (index) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: _buildLabeledRow( - "Email Label", - emailLabels[index], - ["Office", "Personal", "Other"], - "Email", - emailControllers[index], - TextInputType.emailAddress, - onRemove: emailControllers.length > 1 + label: label, + selectedLabel: labels[index], + options: labelOptions, + inputLabel: inputLabel, + controller: ctrls[index], + inputType: inputType, + onRemove: ctrls.length > 1 ? () { - emailControllers.removeAt(index); - emailLabels.removeAt(index); + ctrls.removeAt(index); + labels.removeAt(index); } : null, + suffixIcon: phoneSuffixIcon != null && inputType == TextInputType.phone + ? IconButton( + icon: const Icon(Icons.contact_phone, color: Colors.blue), + onPressed: () async { + final selectedPhone = + await ContactPickerHelper.pickIndianPhoneNumber(context); + if (selectedPhone != null) { + ctrls[index].text = selectedPhone; + } + }, + ) + : null, ), ); }), ); + }); + } - Widget _buildPhoneList() => Column( - children: List.generate(phoneControllers.length, (index) { - return Padding( - padding: const EdgeInsets.only(bottom: 12), - child: _buildLabeledRow( - "Phone Label", - phoneLabels[index], - ["Work", "Mobile", "Other"], - "Phone", - phoneControllers[index], - TextInputType.phone, - onRemove: phoneControllers.length > 1 - ? () { - phoneControllers.removeAt(index); - phoneLabels.removeAt(index); - } - : null, - ), - ); - }), + Widget _buildEmailList() => _buildDynamicList( + ctrls: emailControllers, + labels: emailLabels, + labelOptions: ["Office", "Personal", "Other"], + label: "Email Label", + inputLabel: "Email", + inputType: TextInputType.emailAddress, + listToRemoveFrom: emailControllers, + ); + + Widget _buildPhoneList() => _buildDynamicList( + ctrls: phoneControllers, + labels: phoneLabels, + labelOptions: ["Work", "Mobile", "Other"], + label: "Phone Label", + inputLabel: "Phone", + inputType: TextInputType.phone, + listToRemoveFrom: phoneControllers, + phoneSuffixIcon: const Icon(Icons.contact_phone, color: Colors.blue), ); Widget _popupSelector({ required String hint, required RxString selectedValue, required List options, - }) { - return Obx(() => GestureDetector( - onTap: () async { - final selected = await showMenu( - context: context, - position: RelativeRect.fromLTRB(100, 300, 100, 0), - items: options.map((option) { - return PopupMenuItem( - value: option, - child: Text(option), - ); - }).toList(), - ); - - if (selected != null) { - selectedValue.value = selected; - } - }, - 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), + }) => + Obx(() => GestureDetector( + onTap: () async { + final selected = await showMenu( + context: context, + position: RelativeRect.fromLTRB(100, 300, 100, 0), + items: options.map((option) => PopupMenuItem(value: option, child: Text(option))).toList(), + ); + if (selected != null) selectedValue.value = selected; + }, + 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: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + selectedValue.value.isNotEmpty ? selectedValue.value : hint, + style: const TextStyle(fontSize: 14), + ), + const Icon(Icons.expand_more, size: 20), + ], + ), ), - alignment: Alignment.centerLeft, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - selectedValue.value.isNotEmpty ? selectedValue.value : hint, - style: const TextStyle(fontSize: 14), - ), - const Icon(Icons.expand_more, size: 20), - ], - ), - ), - )); - } + )); Widget _sectionLabel(String title) => Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -332,6 +327,7 @@ class _AddContactBottomSheetState extends State { ], ); + // CHIP list for tags Widget _tagInputSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -350,16 +346,14 @@ class _AddContactBottomSheetState extends State { ), ), Obx(() => controller.filteredSuggestions.isEmpty - ? const SizedBox() + ? const SizedBox.shrink() : Container( margin: const EdgeInsets.only(top: 4), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(8), - boxShadow: const [ - BoxShadow(color: Colors.black12, blurRadius: 4) - ], + boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 4)], ), child: ListView.builder( shrinkWrap: true, @@ -392,145 +386,233 @@ class _AddContactBottomSheetState extends State { ); } - Widget _buildTextField(String label, TextEditingController controller, - {int maxLines = 1}) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.labelMedium(label), - MySpacing.height(8), - TextFormField( - controller: controller, - maxLines: maxLines, - decoration: _inputDecoration("Enter $label"), - validator: (value) => value == null || value.trim().isEmpty - ? "$label is required" - : null, - ), - ], - ); - } + // ---- REQUIRED FIELD (reusable) + Widget _buildTextField( + String label, + TextEditingController controller, { + int maxLines = 1, + bool required = false, + }) => + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.labelMedium(label), + MySpacing.height(8), + TextFormField( + controller: controller, + maxLines: maxLines, + decoration: _inputDecoration("Enter $label"), + validator: required + ? (value) => + value == null || value.trim().isEmpty ? "$label is required" : null + : null, + ), + ], + ); - Widget _buildOrganizationField() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.labelMedium("Organization"), - MySpacing.height(8), - TextField( - controller: orgController, - onChanged: controller.filterOrganizationSuggestions, - decoration: _inputDecoration("Enter organization"), - ), - Obx(() => controller.filteredOrgSuggestions.isEmpty - ? const SizedBox() - : ListView.builder( - shrinkWrap: true, - itemCount: controller.filteredOrgSuggestions.length, - itemBuilder: (context, index) { - final suggestion = controller.filteredOrgSuggestions[index]; - return ListTile( - dense: true, - title: Text(suggestion), - onTap: () { - orgController.text = suggestion; - controller.filteredOrgSuggestions.clear(); - }, + // -- Organization as required TextFormField + Widget _buildOrganizationField() => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.labelMedium("Organization"), + MySpacing.height(8), + TextFormField( + controller: orgController, + onChanged: controller.filterOrganizationSuggestions, + decoration: _inputDecoration("Enter organization"), + validator: (value) => + value == null || value.trim().isEmpty ? "Organization is required" : null, + ), + Obx(() => controller.filteredOrgSuggestions.isEmpty + ? const SizedBox.shrink() + : ListView.builder( + shrinkWrap: true, + itemCount: controller.filteredOrgSuggestions.length, + itemBuilder: (context, index) { + final suggestion = controller.filteredOrgSuggestions[index]; + return ListTile( + dense: true, + title: Text(suggestion), + onTap: () { + orgController.text = suggestion; + controller.filteredOrgSuggestions.clear(); + }, + ); + }, + )), + ], + ); + + // Action button row + Widget _buildActionButtons() => Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () { + Get.back(); + Get.delete(); + }, + icon: const Icon(Icons.close, color: Colors.red), + label: MyText.bodyMedium("Cancel", color: Colors.red, fontWeight: 600), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.red), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + ), + ), + ), + MySpacing.width(12), + Expanded( + child: ElevatedButton.icon( + onPressed: () { + // Validate bucket first in UI and show error under dropdown if empty + bool valid = formKey.currentState!.validate(); + if (controller.selectedBucket.value.isEmpty) { + bucketError.value = "Bucket is required"; + valid = false; + } else { + bucketError.value = ""; + } + if (valid) { + final emails = emailControllers + .asMap() + .entries + .where((entry) => entry.value.text.trim().isNotEmpty) + .map((entry) => { + "label": emailLabels[entry.key].value, + "emailAddress": entry.value.text.trim(), + }) + .toList(); + final phones = phoneControllers + .asMap() + .entries + .where((entry) => entry.value.text.trim().isNotEmpty) + .map((entry) => { + "label": phoneLabels[entry.key].value, + "phoneNumber": entry.value.text.trim(), + }) + .toList(); + controller.submitContact( + id: widget.existingContact?.id, + name: nameController.text.trim(), + organization: orgController.text.trim(), + emails: emails, + phones: phones, + address: addressController.text.trim(), + description: descriptionController.text.trim(), ); - }, - )), - ], - ); - } - - Widget _buildActionButtons() { - return Row( - children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: () { - Get.back(); - Get.delete(); - }, - icon: const Icon(Icons.close, color: Colors.red), - label: - MyText.bodyMedium("Cancel", color: Colors.red, fontWeight: 600), - style: OutlinedButton.styleFrom( - side: const BorderSide(color: Colors.red), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), - ), - ), - ), - MySpacing.width(12), - Expanded( - child: ElevatedButton.icon( - onPressed: () { - if (formKey.currentState!.validate()) { - final emails = emailControllers - .asMap() - .entries - .where((entry) => entry.value.text.trim().isNotEmpty) - .map((entry) => { - "label": emailLabels[entry.key].value, - "emailAddress": entry.value.text.trim(), - }) - .toList(); - - final phones = phoneControllers - .asMap() - .entries - .where((entry) => entry.value.text.trim().isNotEmpty) - .map((entry) => { - "label": phoneLabels[entry.key].value, - "phoneNumber": entry.value.text.trim(), - }) - .toList(); - - controller.submitContact( - id: widget.existingContact?.id, - name: nameController.text.trim(), - organization: orgController.text.trim(), - emails: emails, - phones: phones, - address: addressController.text.trim(), - description: descriptionController.text.trim(), - ); - } - }, - icon: const Icon(Icons.check_circle_outline, color: Colors.white), - label: - MyText.bodyMedium("Save", color: Colors.white, fontWeight: 600), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.indigo, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), - ), - ), - ), - ], + } + }, + icon: const Icon(Icons.check_circle_outline, color: Colors.white), + label: MyText.bodyMedium("Save", color: Colors.white, fontWeight: 600), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.indigo, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + ), + ), + ), + ], + ); + + // Projects multi-select section + Widget _projectSelectorUI() { + return GestureDetector( + onTap: () async { + await showDialog( + context: context, + builder: (_) { + return AlertDialog( + title: const Text('Select Projects'), + content: Obx(() => 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( + checkboxTheme: CheckboxThemeData( + fillColor: MaterialStateProperty.resolveWith( + (states) => states.contains(MaterialState.selected) + ? Colors.white + : 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: (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), + ], + ); + }), + ), ); } + // --- MAIN BUILD --- @override Widget build(BuildContext context) { return Obx(() { if (!controller.isInitialized.value) { return const Center(child: CircularProgressIndicator()); } - return SafeArea( child: SingleChildScrollView( - padding: EdgeInsets.only( - top: 32, - ).add(MediaQuery.of(context).viewInsets), + padding: EdgeInsets.only(top: 32).add(MediaQuery.of(context).viewInsets), child: Container( decoration: BoxDecoration( color: Theme.of(context).cardColor, - borderRadius: - const BorderRadius.vertical(top: Radius.circular(24)), + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), ), child: Padding( padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), @@ -541,25 +623,44 @@ class _AddContactBottomSheetState extends State { children: [ Center( child: MyText.titleMedium( - widget.existingContact != null - ? "Edit Contact" - : "Create New Contact", + widget.existingContact != null ? "Edit Contact" : "Create New Contact", fontWeight: 700, ), ), MySpacing.height(24), _sectionLabel("Required Fields"), MySpacing.height(12), - _buildTextField("Name", nameController), + _buildTextField("Name", nameController, required: true), MySpacing.height(16), _buildOrganizationField(), MySpacing.height(16), MyText.labelMedium("Select Bucket"), MySpacing.height(8), - _popupSelector( - hint: "Select Bucket", - selectedValue: controller.selectedBucket, - options: controller.buckets, + Stack( + children: [ + _popupSelector( + hint: "Select Bucket", + selectedValue: controller.selectedBucket, + options: controller.buckets, + ), + // Validation message for bucket + Positioned( + left: 0, + right: 0, + top: 56, + child: Obx( + () => bucketError.value.isEmpty + ? const SizedBox.shrink() + : Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), + child: Text( + bucketError.value, + style: const TextStyle(color: Colors.red, fontSize: 12), + ), + ), + ), + ), + ], ), MySpacing.height(24), Obx(() => GestureDetector( @@ -567,11 +668,8 @@ class _AddContactBottomSheetState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - MyText.labelLarge("Advanced Details (Optional)", - fontWeight: 600), - Icon(showAdvanced.value - ? Icons.expand_less - : Icons.expand_more), + MyText.labelLarge("Advanced Details (Optional)", fontWeight: 600), + Icon(showAdvanced.value ? Icons.expand_less : Icons.expand_more), ], ), )), @@ -619,15 +717,12 @@ class _AddContactBottomSheetState extends State { MySpacing.height(8), _tagInputSection(), MySpacing.height(16), - _buildTextField("Address", addressController, - maxLines: 2), + _buildTextField("Address", addressController, maxLines: 2, required: false), MySpacing.height(16), - _buildTextField( - "Description", descriptionController, - maxLines: 2), + _buildTextField("Description", descriptionController, maxLines: 2, required: false), ], ) - : const SizedBox()), + : const SizedBox.shrink()), MySpacing.height(24), _buildActionButtons(), ], @@ -639,95 +734,4 @@ class _AddContactBottomSheetState extends State { ); }); } - - 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), - ], - ); - }), - ), - ); - } } diff --git a/lib/view/directory/contact_detail_screen.dart b/lib/view/directory/contact_detail_screen.dart index 9550f70..f670c18 100644 --- a/lib/view/directory/contact_detail_screen.dart +++ b/lib/view/directory/contact_detail_screen.dart @@ -16,32 +16,20 @@ import 'package:marco/model/directory/add_comment_bottom_sheet.dart'; import 'package:marco/helpers/utils/date_time_utils.dart'; import 'package:marco/model/directory/add_contact_bottom_sheet.dart'; -class ContactDetailScreen extends StatefulWidget { - final ContactModel contact; - - const ContactDetailScreen({super.key, required this.contact}); - - @override - State createState() => _ContactDetailScreenState(); -} - +// HELPER: Delta to HTML conversion String _convertDeltaToHtml(dynamic delta) { final buffer = StringBuffer(); bool inList = false; for (var op in delta.toList()) { - final data = op.data?.toString() ?? ''; + final String data = op.data?.toString() ?? ''; final attr = op.attributes ?? {}; + final bool isListItem = attr.containsKey('list'); - final isListItem = attr.containsKey('list'); - - // Start list if (isListItem && !inList) { buffer.write('
    '); inList = true; } - - // Close list if we are not in list mode anymore if (!isListItem && inList) { buffer.write('
'); inList = false; @@ -49,15 +37,12 @@ String _convertDeltaToHtml(dynamic delta) { if (isListItem) buffer.write('
  • '); - // Apply inline styles if (attr.containsKey('bold')) buffer.write(''); if (attr.containsKey('italic')) buffer.write(''); if (attr.containsKey('underline')) buffer.write(''); if (attr.containsKey('strike')) buffer.write(''); if (attr.containsKey('link')) buffer.write(''); - buffer.write(data.replaceAll('\n', '')); - if (attr.containsKey('link')) buffer.write(''); if (attr.containsKey('strike')) buffer.write(''); if (attr.containsKey('underline')) buffer.write(''); @@ -66,14 +51,21 @@ String _convertDeltaToHtml(dynamic delta) { if (isListItem) buffer.write('
  • '); - else if (data.contains('\n')) buffer.write('
    '); + else if (data.contains('\n')) { + buffer.write('
    '); + } } - if (inList) buffer.write(''); - return buffer.toString(); } +class ContactDetailScreen extends StatefulWidget { + final ContactModel contact; + const ContactDetailScreen({super.key, required this.contact}); + @override + State createState() => _ContactDetailScreenState(); +} + class _ContactDetailScreenState extends State { late final DirectoryController directoryController; late final ProjectController projectController; @@ -85,7 +77,6 @@ class _ContactDetailScreenState extends State { directoryController = Get.find(); projectController = Get.find(); contact = widget.contact; - WidgetsBinding.instance.addPostFrameCallback((_) { directoryController.fetchCommentsForContact(contact.id); }); @@ -103,13 +94,12 @@ class _ContactDetailScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSubHeader(), + const Divider(height: 1, thickness: 0.5, color: Colors.grey), Expanded( - child: TabBarView( - children: [ - _buildDetailsTab(), - _buildCommentsTab(context), - ], - ), + child: TabBarView(children: [ + _buildDetailsTab(), + _buildCommentsTab(context), + ]), ), ], ), @@ -130,10 +120,8 @@ class _ContactDetailScreenState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( - icon: const Icon(Icons.arrow_back_ios_new, - color: Colors.black, size: 20), - onPressed: () => - Get.offAllNamed('/dashboard/directory-main-page'), + icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black, size: 20), + onPressed: () => Get.offAllNamed('/dashboard/directory-main-page'), ), MySpacing.width(8), Expanded( @@ -141,30 +129,10 @@ class _ContactDetailScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - MyText.titleLarge('Contact Profile', - fontWeight: 700, color: Colors.black), + MyText.titleLarge('Contact Profile', fontWeight: 700, color: Colors.black), MySpacing.height(2), GetBuilder( - builder: (projectController) { - final projectName = - projectController.selectedProject?.name ?? - 'Select Project'; - return Row( - children: [ - const Icon(Icons.work_outline, - size: 14, color: Colors.grey), - MySpacing.width(4), - Expanded( - child: MyText.bodySmall( - projectName, - fontWeight: 600, - overflow: TextOverflow.ellipsis, - color: Colors.grey[700], - ), - ), - ], - ); - }, + builder: (p) => ProjectLabel(p.selectedProject?.name), ), ], ), @@ -176,38 +144,30 @@ class _ContactDetailScreenState extends State { } Widget _buildSubHeader() { + final firstName = contact.name.split(" ").first; + final lastName = contact.name.split(" ").length > 1 ? contact.name.split(" ").last : ""; + return Padding( padding: MySpacing.xy(16, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Avatar( - firstName: contact.name.split(" ").first, - lastName: contact.name.split(" ").length > 1 - ? contact.name.split(" ").last - : "", - size: 35, - backgroundColor: Colors.indigo, - ), - MySpacing.width(12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.titleSmall(contact.name, - fontWeight: 600, color: Colors.black), - MySpacing.height(2), - MyText.bodySmall(contact.organization, - fontWeight: 500, color: Colors.grey[700]), - ], - ), - ], - ), + Row(children: [ + Avatar(firstName: firstName, lastName: lastName, size: 35, backgroundColor: Colors.indigo), + MySpacing.width(12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleSmall(contact.name, fontWeight: 600, color: Colors.black), + MySpacing.height(2), + MyText.bodySmall(contact.organization, fontWeight: 500, color: Colors.grey[700]), + ], + ), + ]), TabBar( labelColor: Colors.red, unselectedLabelColor: Colors.black, - indicator: MaterialIndicator( + indicator: MaterialIndicator( color: Colors.red, height: 4, topLeftRadius: 8, @@ -226,33 +186,37 @@ class _ContactDetailScreenState extends State { } Widget _buildDetailsTab() { - final email = contact.contactEmails.isNotEmpty - ? contact.contactEmails.first.emailAddress - : "-"; - - final phone = contact.contactPhones.isNotEmpty - ? contact.contactPhones.first.phoneNumber - : "-"; - final tags = contact.tags.map((e) => e.name).join(", "); - final bucketNames = contact.bucketIds .map((id) => directoryController.contactBuckets .firstWhereOrNull((b) => b.id == id) ?.name) .whereType() .join(", "); - - final projectNames = contact.projectIds - ?.map((id) => projectController.projects - .firstWhereOrNull((p) => p.id == id) - ?.name) - .whereType() - .join(", ") ?? - "-"; - + final projectNames = contact.projectIds?.map((id) => + projectController.projects.firstWhereOrNull((p) => p.id == id)?.name).whereType().join(", ") ?? "-"; final category = contact.contactCategory?.name ?? "-"; + Widget multiRows({required List items, required IconData icon, required String label, required String typeLabel, required Function(String)? onTap, required Function(String)? onLongPress}) { + return items.isNotEmpty + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _iconInfoRow(icon, label, items.first, onTap: () => onTap?.call(items.first), onLongPress: () => onLongPress?.call(items.first)), + ...items.skip(1).map( + (val) => _iconInfoRow( + null, + '', + val, + onTap: () => onTap?.call(val), + onLongPress: () => onLongPress?.call(val), + ), + ), + ], + ) + : _iconInfoRow(icon, label, "-"); + } + return Stack( children: [ SingleChildScrollView( @@ -261,28 +225,38 @@ class _ContactDetailScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ MySpacing.height(12), + // BASIC INFO CARD _infoCard("Basic Info", [ - _iconInfoRow(Icons.email, "Email", email, - onTap: () => LauncherUtils.launchEmail(email), - onLongPress: () => LauncherUtils.copyToClipboard(email, - typeLabel: "Email")), - _iconInfoRow(Icons.phone, "Phone", phone, - onTap: () => LauncherUtils.launchPhone(phone), - onLongPress: () => LauncherUtils.copyToClipboard(phone, - typeLabel: "Phone")), + multiRows( + items: contact.contactEmails.map((e) => e.emailAddress).toList(), + icon: Icons.email, + label: "Email", + typeLabel: "Email", + onTap: (email) => LauncherUtils.launchEmail(email), + onLongPress: (email) => LauncherUtils.copyToClipboard(email, typeLabel: "Email"), + ), + multiRows( + items: contact.contactPhones.map((p) => p.phoneNumber).toList(), + icon: Icons.phone, + label: "Phone", + typeLabel: "Phone", + onTap: (phone) => LauncherUtils.launchPhone(phone), + onLongPress: (phone) => LauncherUtils.copyToClipboard(phone, typeLabel: "Phone"), + ), _iconInfoRow(Icons.location_on, "Address", contact.address), ]), + // ORGANIZATION CARD _infoCard("Organization", [ - _iconInfoRow( - Icons.business, "Organization", contact.organization), + _iconInfoRow(Icons.business, "Organization", contact.organization), _iconInfoRow(Icons.category, "Category", category), ]), + // META INFO CARD _infoCard("Meta Info", [ _iconInfoRow(Icons.label, "Tags", tags.isNotEmpty ? tags : "-"), - _iconInfoRow(Icons.folder_shared, "Contact Buckets", - bucketNames.isNotEmpty ? bucketNames : "-"), + _iconInfoRow(Icons.folder_shared, "Contact Buckets", bucketNames.isNotEmpty ? bucketNames : "-"), _iconInfoRow(Icons.work_outline, "Projects", projectNames), ]), + // DESCRIPTION CARD _infoCard("Description", [ MySpacing.height(6), Align( @@ -294,7 +268,7 @@ class _ContactDetailScreenState extends State { textAlign: TextAlign.left, ), ), - ]) + ]), ], ), ), @@ -309,25 +283,17 @@ class _ContactDetailScreenState extends State { isScrollControlled: true, backgroundColor: Colors.transparent, ); - if (result == true) { await directoryController.fetchContacts(); final updated = - directoryController.allContacts.firstWhereOrNull( - (c) => c.id == contact.id, - ); + directoryController.allContacts.firstWhereOrNull((c) => c.id == contact.id); if (updated != null) { - setState(() { - contact = updated; - }); + setState(() => contact = updated); } } }, icon: const Icon(Icons.edit, color: Colors.white), - label: const Text( - "Edit Contact", - style: TextStyle(color: Colors.white), - ), + label: const Text("Edit Contact", style: TextStyle(color: Colors.white)), ), ), ], @@ -337,24 +303,17 @@ class _ContactDetailScreenState extends State { Widget _buildCommentsTab(BuildContext context) { return Obx(() { final contactId = contact.id; - if (!directoryController.contactCommentsMap.containsKey(contactId)) { return const Center(child: CircularProgressIndicator()); } - - final comments = directoryController - .getCommentsForContact(contactId) - .reversed - .toList(); - + final comments = directoryController.getCommentsForContact(contactId).reversed.toList(); final editingId = directoryController.editingCommentId.value; return Stack( children: [ comments.isEmpty - ? Center( - child: - MyText.bodyLarge("No comments yet.", color: Colors.grey), + ? Center( + child: MyText.bodyLarge("No comments yet.", color: Colors.grey), ) : Padding( padding: MySpacing.xy(12, 12), @@ -362,137 +321,10 @@ class _ContactDetailScreenState extends State { padding: const EdgeInsets.only(bottom: 100), itemCount: comments.length, separatorBuilder: (_, __) => MySpacing.height(14), - itemBuilder: (_, index) { - final comment = comments[index]; - final isEditing = editingId == comment.id; - - final initials = comment.createdBy.firstName.isNotEmpty - ? comment.createdBy.firstName[0].toUpperCase() - : "?"; - - final decodedDelta = HtmlToDelta().convert(comment.note); - - final quillController = isEditing - ? quill.QuillController( - document: quill.Document.fromDelta(decodedDelta), - selection: TextSelection.collapsed( - offset: decodedDelta.length), - ) - : null; - - return AnimatedContainer( - duration: const Duration(milliseconds: 300), - padding: MySpacing.xy(8, 7), - decoration: BoxDecoration( - color: isEditing ? Colors.indigo[50] : Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isEditing - ? Colors.indigo - : Colors.grey.shade300, - width: 1.2, - ), - boxShadow: const [ - BoxShadow( - color: Colors.black12, - blurRadius: 4, - offset: Offset(0, 2), - ) - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header Row - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Avatar( - firstName: initials, - lastName: '', - size: 36), - MySpacing.width(12), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - MyText.bodyMedium( - "By: ${comment.createdBy.firstName}", - fontWeight: 600, - color: Colors.indigo[800], - ), - MySpacing.height(4), - MyText.bodySmall( - DateTimeUtils.convertUtcToLocal( - comment.createdAt.toString(), - format: 'dd MMM yyyy, hh:mm a', - ), - color: Colors.grey[600], - ), - ], - ), - ), - IconButton( - icon: Icon( - isEditing ? Icons.close : Icons.edit, - size: 20, - color: Colors.indigo, - ), - onPressed: () { - directoryController.editingCommentId.value = - isEditing ? null : comment.id; - }, - ), - ], - ), - // Comment Content - if (isEditing && quillController != null) - CommentEditorCard( - controller: quillController, - onCancel: () { - directoryController.editingCommentId.value = - null; - }, - onSave: (controller) async { - final delta = controller.document.toDelta(); - final htmlOutput = _convertDeltaToHtml(delta); - final updated = - comment.copyWith(note: htmlOutput); - - await directoryController - .updateComment(updated); - - // ✅ Re-fetch comments to get updated list - await directoryController - .fetchCommentsForContact(contactId); - - // ✅ Exit editing mode - directoryController.editingCommentId.value = - null; - }, - ) - else - html.Html( - data: comment.note, - style: { - "body": html.Style( - margin: html.Margins.zero, - padding: html.HtmlPaddings.zero, - fontSize: html.FontSize.medium, - color: Colors.black87, - ), - }, - ), - ], - ), - ); - }, + itemBuilder: (_, index) => _buildCommentItem(comments[index], editingId, contact.id), ), ), - - // Floating Action Button - if (directoryController.editingCommentId.value == null) + if (editingId == null) Positioned( bottom: 20, right: 20, @@ -503,17 +335,12 @@ class _ContactDetailScreenState extends State { AddCommentBottomSheet(contactId: contactId), isScrollControlled: true, ); - if (result == true) { - await directoryController - .fetchCommentsForContact(contactId); + await directoryController.fetchCommentsForContact(contactId); } }, icon: const Icon(Icons.add_comment, color: Colors.white), - label: const Text( - "Add Comment", - style: TextStyle(color: Colors.white), - ), + label: const Text("Add Comment", style: TextStyle(color: Colors.white)), ), ), ], @@ -521,25 +348,127 @@ class _ContactDetailScreenState extends State { }); } - Widget _iconInfoRow(IconData icon, String label, String value, - {VoidCallback? onTap, VoidCallback? onLongPress}) { + Widget _buildCommentItem(comment, editingId, contactId) { + final isEditing = editingId == comment.id; + final initials = comment.createdBy.firstName.isNotEmpty + ? comment.createdBy.firstName[0].toUpperCase() + : "?"; + final decodedDelta = HtmlToDelta().convert(comment.note); + final quillController = isEditing + ? quill.QuillController( + document: quill.Document.fromDelta(decodedDelta), + selection: TextSelection.collapsed(offset: decodedDelta.length), + ) + : null; + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + padding: MySpacing.xy(8, 7), + decoration: BoxDecoration( + color: isEditing ? Colors.indigo[50] : Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isEditing ? Colors.indigo : Colors.grey.shade300, + width: 1.2, + ), + boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 4, offset: Offset(0, 2))], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Row + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Avatar(firstName: initials, lastName: '', size: 36), + MySpacing.width(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyMedium("By: ${comment.createdBy.firstName}", + fontWeight: 600, color: Colors.indigo[800]), + MySpacing.height(4), + MyText.bodySmall( + DateTimeUtils.convertUtcToLocal( + comment.createdAt.toString(), + format: 'dd MMM yyyy, hh:mm a', + ), + color: Colors.grey[600], + ), + ], + ), + ), + IconButton( + icon: Icon( + isEditing ? Icons.close : Icons.edit, + size: 20, + color: Colors.indigo, + ), + onPressed: () { + directoryController.editingCommentId.value = isEditing ? null : comment.id; + }, + ), + ], + ), + // Comment Content + if (isEditing && quillController != null) + CommentEditorCard( + controller: quillController, + onCancel: () => directoryController.editingCommentId.value = null, + onSave: (ctrl) async { + final delta = ctrl.document.toDelta(); + final htmlOutput = _convertDeltaToHtml(delta); + final updated = comment.copyWith(note: htmlOutput); + await directoryController.updateComment(updated); + await directoryController.fetchCommentsForContact(contactId); + directoryController.editingCommentId.value = null; + }, + ) + else + html.Html( + data: comment.note, + style: { + "body": html.Style( + margin: html.Margins.zero, + padding: html.HtmlPaddings.zero, + fontSize: html.FontSize.medium, + color: Colors.black87, + ), + }, + ), + ], + ), + ); + } + + Widget _iconInfoRow( + IconData? icon, + String label, + String value, { + VoidCallback? onTap, + VoidCallback? onLongPress, + }) { return Padding( - padding: MySpacing.y(8), + padding: MySpacing.y(2), child: GestureDetector( onTap: onTap, onLongPress: onLongPress, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(icon, size: 22, color: Colors.indigo), - MySpacing.width(12), + if (icon != null) ...[ + Icon(icon, size: 22, color: Colors.indigo), + MySpacing.width(12), + ] else + const SizedBox(width: 34), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - MyText.bodySmall(label, - fontWeight: 600, color: Colors.black87), - MySpacing.height(2), + if (label.isNotEmpty) + MyText.bodySmall(label, fontWeight: 600, color: Colors.black87), + if (label.isNotEmpty) MySpacing.height(2), MyText.bodyMedium(value, color: Colors.grey[800]), ], ), @@ -560,8 +489,7 @@ class _ContactDetailScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - MyText.titleSmall(title, - fontWeight: 700, color: Colors.indigo[700]), + MyText.titleSmall(title, fontWeight: 700, color: Colors.indigo[700]), MySpacing.height(8), ...children, ], @@ -570,3 +498,26 @@ class _ContactDetailScreenState extends State { ); } } + +// Helper widget for Project label in AppBar +class ProjectLabel extends StatelessWidget { + final String? projectName; + const ProjectLabel(this.projectName, {super.key}); + @override + Widget build(BuildContext context) { + return Row( + children: [ + const Icon(Icons.work_outline, size: 14, color: Colors.grey), + MySpacing.width(4), + Expanded( + child: MyText.bodySmall( + projectName ?? 'Select Project', + fontWeight: 600, + overflow: TextOverflow.ellipsis, + color: Colors.grey[700], + ), + ), + ], + ); + } +} diff --git a/lib/view/directory/directory_view.dart b/lib/view/directory/directory_view.dart index 2230a24..1cecd6c 100644 --- a/lib/view/directory/directory_view.dart +++ b/lib/view/directory/directory_view.dart @@ -24,8 +24,7 @@ class DirectoryView extends StatefulWidget { class _DirectoryViewState extends State { final DirectoryController controller = Get.find(); final TextEditingController searchController = TextEditingController(); - final PermissionController permissionController = - Get.put(PermissionController()); + final PermissionController permissionController = Get.put(PermissionController()); Future _refreshDirectory() async { try { @@ -304,7 +303,6 @@ class _DirectoryViewState extends State { backgroundColor: Colors.transparent, builder: (_) => const CreateBucketBottomSheet(), ); - if (created == true) { await controller.fetchBuckets(); } @@ -442,62 +440,69 @@ class _DirectoryViewState extends State { color: Colors.grey[700], overflow: TextOverflow.ellipsis), MySpacing.height(8), - ...contact.contactEmails.map((e) => - GestureDetector( - onTap: () => LauncherUtils.launchEmail( - e.emailAddress), - onLongPress: () => - LauncherUtils.copyToClipboard( - e.emailAddress, - typeLabel: 'Email'), - child: Padding( - padding: - const EdgeInsets.only(bottom: 4), - child: Row( - children: [ - const Icon(Icons.email_outlined, - size: 16, color: Colors.indigo), - MySpacing.width(4), - ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 180), - child: MyText.labelSmall( - e.emailAddress, - overflow: TextOverflow.ellipsis, - color: Colors.indigo, - decoration: - TextDecoration.underline, - ), - ), - ], - ), - ), - )), - ...contact.contactPhones.map((p) => Padding( - padding: const EdgeInsets.only( - bottom: 8, top: 4), + + // Show only the first email (if present) + if (contact.contactEmails.isNotEmpty) + GestureDetector( + onTap: () => LauncherUtils.launchEmail( + contact.contactEmails.first.emailAddress), + onLongPress: () => + LauncherUtils.copyToClipboard( + contact.contactEmails.first.emailAddress, + typeLabel: 'Email', + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 4), child: Row( children: [ - GestureDetector( - onTap: () => - LauncherUtils.launchPhone( - p.phoneNumber), + const Icon(Icons.email_outlined, + size: 16, color: Colors.indigo), + MySpacing.width(4), + Expanded( + child: MyText.labelSmall( + contact.contactEmails.first.emailAddress, + overflow: TextOverflow.ellipsis, + color: Colors.indigo, + decoration: + TextDecoration.underline, + ), + ), + ], + ), + ), + ), + + // Show only the first phone (if present) + if (contact.contactPhones.isNotEmpty) + Padding( + padding: const EdgeInsets.only( + bottom: 8, top: 4), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => LauncherUtils + .launchPhone(contact + .contactPhones + .first + .phoneNumber), onLongPress: () => LauncherUtils.copyToClipboard( - p.phoneNumber, - typeLabel: 'Phone'), + contact.contactPhones.first + .phoneNumber, + typeLabel: 'Phone', + ), child: Row( children: [ - const Icon(Icons.phone_outlined, + const Icon( + Icons.phone_outlined, size: 16, color: Colors.indigo), MySpacing.width(4), - ConstrainedBox( - constraints: - const BoxConstraints( - maxWidth: 140), + Expanded( child: MyText.labelSmall( - p.phoneNumber, + contact.contactPhones.first + .phoneNumber, overflow: TextOverflow.ellipsis, color: Colors.indigo, @@ -508,19 +513,22 @@ class _DirectoryViewState extends State { ], ), ), - MySpacing.width(8), - GestureDetector( - onTap: () => - LauncherUtils.launchWhatsApp( - p.phoneNumber), - child: const FaIcon( - FontAwesomeIcons.whatsapp, - color: Colors.green, - size: 16), + ), + MySpacing.width(8), + GestureDetector( + onTap: () => + LauncherUtils.launchWhatsApp( + contact.contactPhones.first + .phoneNumber), + child: const FaIcon( + FontAwesomeIcons.whatsapp, + color: Colors.green, + size: 16, ), - ], - ), - )), + ), + ], + ), + ), if (tags.isNotEmpty) ...[ MySpacing.height(2), MyText.labelSmall(tags.join(', '),