From e7940941edfc7016e82a4618be7d66d84077e161 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Sat, 5 Jul 2025 13:19:53 +0530 Subject: [PATCH] feat(directory): enhance AddContact functionality to support multiple emails and phones, improve logging, and refactor contact detail display --- .../directory/add_contact_controller.dart | 69 ++--- lib/helpers/services/api_service.dart | 49 ++- lib/helpers/utils/date_time_utils.dart | 38 +++ .../directory/add_contact_bottom_sheet.dart | 287 +++++++++++------- lib/view/directory/contact_detail_screen.dart | 12 +- lib/view/directory/directory_main_screen.dart | 144 ++++----- 6 files changed, 369 insertions(+), 230 deletions(-) create mode 100644 lib/helpers/utils/date_time_utils.dart diff --git a/lib/controller/directory/add_contact_controller.dart b/lib/controller/directory/add_contact_controller.dart index b3f3b43..7a78231 100644 --- a/lib/controller/directory/add_contact_controller.dart +++ b/lib/controller/directory/add_contact_controller.dart @@ -1,3 +1,5 @@ +// Updated AddContactController to support multiple emails and phones + import 'package:get/get.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/services/app_logger.dart'; @@ -62,10 +64,10 @@ class AddContactController extends GetxController { } } buckets.assignAll(names); - logSafe("Fetched ${names.length} buckets"); + logSafe("Fetched \${names.length} buckets"); } } catch (e) { - logSafe("Failed to fetch buckets: $e", level: LogLevel.error); + logSafe("Failed to fetch buckets: \$e", level: LogLevel.error); } } @@ -73,19 +75,17 @@ class AddContactController extends GetxController { try { final orgs = await ApiService.getOrganizationList(); organizationNames.assignAll(orgs); - logSafe("Fetched ${orgs.length} organization names"); + logSafe("Fetched \${orgs.length} organization names"); } catch (e) { - logSafe("Failed to load organization names: $e", level: LogLevel.error); + logSafe("Failed to load organization names: \$e", level: LogLevel.error); } } Future submitContact({ required String name, required String organization, - required String email, - required String emailLabel, - required String phone, - required String phoneLabel, + required List> emails, + required List> phones, required String address, required String description, }) async { @@ -96,9 +96,7 @@ class AddContactController extends GetxController { final tagObjects = enteredTags.map((tagName) { final tagId = tagsMap[tagName]; - return tagId != null - ? {"id": tagId, "name": tagName} - : {"name": tagName}; + return tagId != null ? {"id": tagId, "name": tagName} : {"name": tagName}; }).toList(); final body = { @@ -108,18 +106,8 @@ class AddContactController extends GetxController { "projectIds": projectId != null ? [projectId] : [], "bucketIds": bucketId != null ? [bucketId] : [], "tags": tagObjects, - "contactEmails": [ - { - "label": emailLabel, - "emailAddress": email, - } - ], - "contactPhones": [ - { - "label": phoneLabel, - "phoneNumber": phone, - } - ], + "contactEmails": emails, + "contactPhones": phones, "address": address, "description": description, }; @@ -129,10 +117,7 @@ class AddContactController extends GetxController { final response = await ApiService.createContact(body); if (response == true) { logSafe("Contact creation succeeded"); - - // Send result back to previous screen Get.back(result: true); - showAppSnackbar( title: "Success", message: "Contact created successfully", @@ -147,7 +132,7 @@ class AddContactController extends GetxController { ); } } catch (e) { - logSafe("Contact creation error: $e", level: LogLevel.error); + logSafe("Contact creation error: \$e", level: LogLevel.error); showAppSnackbar( title: "Error", message: "Something went wrong", @@ -164,12 +149,9 @@ class AddContactController extends GetxController { final lower = query.toLowerCase(); filteredOrgSuggestions.assignAll( - organizationNames - .where((name) => name.toLowerCase().contains(lower)) - .toList(), + organizationNames.where((name) => name.toLowerCase().contains(lower)).toList(), ); - logSafe("Filtered organization suggestions for: $query", - level: LogLevel.debug); + logSafe("Filtered organization suggestions for: \$query", level: LogLevel.debug); } Future fetchGlobalProjects() async { @@ -186,10 +168,10 @@ class AddContactController extends GetxController { } } globalProjects.assignAll(names); - logSafe("Fetched ${names.length} global projects"); + logSafe("Fetched \${names.length} global projects"); } } catch (e) { - logSafe("Failed to fetch global projects: $e", level: LogLevel.error); + logSafe("Failed to fetch global projects: \$e", level: LogLevel.error); } } @@ -200,10 +182,10 @@ class AddContactController extends GetxController { tags.assignAll(List.from( response['data'].map((e) => e['name'] ?? '').where((e) => e != ''), )); - logSafe("Fetched ${tags.length} tags"); + logSafe("Fetched \${tags.length} tags"); } } catch (e) { - logSafe("Failed to fetch tags: $e", level: LogLevel.error); + logSafe("Failed to fetch tags: \$e", level: LogLevel.error); } } @@ -215,12 +197,9 @@ class AddContactController extends GetxController { final lower = query.toLowerCase(); filteredSuggestions.assignAll( - tags - .where((tag) => - tag.toLowerCase().contains(lower) && !enteredTags.contains(tag)) - .toList(), + tags.where((tag) => tag.toLowerCase().contains(lower) && !enteredTags.contains(tag)).toList(), ); - logSafe("Filtered tag suggestions for: $query", level: LogLevel.debug); + logSafe("Filtered tag suggestions for: \$query", level: LogLevel.debug); } void clearSuggestions() { @@ -242,22 +221,22 @@ class AddContactController extends GetxController { } } categories.assignAll(names); - logSafe("Fetched ${names.length} contact categories"); + logSafe("Fetched \${names.length} contact categories"); } } catch (e) { - logSafe("Failed to fetch categories: $e", level: LogLevel.error); + logSafe("Failed to fetch categories: \$e", level: LogLevel.error); } } void addEnteredTag(String tag) { if (tag.trim().isNotEmpty && !enteredTags.contains(tag.trim())) { enteredTags.add(tag.trim()); - logSafe("Added tag: $tag", level: LogLevel.debug); + logSafe("Added tag: \$tag", level: LogLevel.debug); } } void removeEnteredTag(String tag) { enteredTags.remove(tag); - logSafe("Removed tag: $tag", level: LogLevel.debug); + logSafe("Removed tag: \$tag", level: LogLevel.debug); } } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index d894c98..3361e9e 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -106,23 +106,45 @@ class ApiService { bool hasRetried = false, }) async { String? token = await _getToken(); - if (token == null) return null; + if (token == null) { + logSafe("Token is null. Cannot proceed with GET request.", + level: LogLevel.error); + return null; + } final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint") .replace(queryParameters: queryParams); - logSafe("GET $uri"); + + logSafe("Initiating GET request", level: LogLevel.debug); + logSafe("URL: $uri", level: LogLevel.debug); + logSafe("Query Parameters: ${queryParams ?? {}}", level: LogLevel.debug); + logSafe("Headers: ${_headers(token)}", level: LogLevel.debug); try { final response = await http.get(uri, headers: _headers(token)).timeout(timeout); + + logSafe("Response Status: ${response.statusCode}", level: LogLevel.debug); + logSafe("Response Body: ${response.body}", level: LogLevel.debug); + if (response.statusCode == 401 && !hasRetried) { - logSafe("Unauthorized. Attempting token refresh..."); + logSafe("Unauthorized (401). Attempting token refresh...", + level: LogLevel.warning); + if (await AuthService.refreshToken()) { - return await _getRequest(endpoint, - queryParams: queryParams, hasRetried: true); + logSafe("Token refresh succeeded. Retrying request...", + level: LogLevel.info); + return await _getRequest( + endpoint, + queryParams: queryParams, + hasRetried: true, + ); } - logSafe("Token refresh failed."); + + logSafe("Token refresh failed. Aborting request.", + level: LogLevel.error); } + return response; } catch (e) { logSafe("HTTP GET Exception: $e", level: LogLevel.error); @@ -324,7 +346,7 @@ class ApiService { static Future createContact(Map payload) async { try { - logSafe("Submitting contact payload: $payload", sensitive: true); + logSafe("Submitting contact payload: $payload"); final response = await _postRequest(ApiEndpoints.createContact, payload); if (response != null) { @@ -345,15 +367,24 @@ class ApiService { static Future> getOrganizationList() async { try { - final response = await _getRequest(ApiEndpoints.getDirectoryOrganization); + final url = ApiEndpoints.getDirectoryOrganization; + logSafe("Sending GET request to: $url", level: LogLevel.info); + + final response = await _getRequest(url); + + logSafe("Response status: ${response?.statusCode}", + level: LogLevel.debug); + logSafe("Response body: ${response?.body}", level: LogLevel.debug); + if (response != null && response.statusCode == 200) { final body = jsonDecode(response.body); if (body['success'] == true && body['data'] is List) { return List.from(body['data']); } } - } catch (e) { + } catch (e, stackTrace) { logSafe("Failed to fetch organization names: $e", level: LogLevel.error); + logSafe("Stack trace: $stackTrace", level: LogLevel.debug); } return []; } diff --git a/lib/helpers/utils/date_time_utils.dart b/lib/helpers/utils/date_time_utils.dart new file mode 100644 index 0000000..2c6a732 --- /dev/null +++ b/lib/helpers/utils/date_time_utils.dart @@ -0,0 +1,38 @@ +import 'package:intl/intl.dart'; +import 'package:marco/helpers/services/app_logger.dart'; + +class DateTimeUtils { + static String convertUtcToLocal(String utcTimeString, {String format = 'dd-MM-yyyy'}) { + try { + logSafe('convertUtcToLocal: input="$utcTimeString", format="$format"'); + + final parsed = DateTime.parse(utcTimeString); + final utcDateTime = DateTime.utc( + parsed.year, + parsed.month, + parsed.day, + parsed.hour, + parsed.minute, + parsed.second, + parsed.millisecond, + parsed.microsecond, + ); + logSafe('Parsed (assumed UTC): $utcDateTime'); + + final localDateTime = utcDateTime.toLocal(); + logSafe('Converted to Local: $localDateTime'); + + final formatted = _formatDateTime(localDateTime, format: format); + logSafe('Formatted Local Time: $formatted'); + + return formatted; + } catch (e, stackTrace) { + logSafe('DateTime conversion failed: $e', error: e, stackTrace: stackTrace); + return 'Invalid Date'; + } + } + + static String _formatDateTime(DateTime dateTime, {String format = 'dd-MM-yyyy'}) { + return DateFormat(format).format(dateTime); + } +} diff --git a/lib/model/directory/add_contact_bottom_sheet.dart b/lib/model/directory/add_contact_bottom_sheet.dart index de2ff9e..4c0337c 100644 --- a/lib/model/directory/add_contact_bottom_sheet.dart +++ b/lib/model/directory/add_contact_bottom_sheet.dart @@ -7,34 +7,38 @@ import 'package:marco/helpers/widgets/my_text_style.dart'; class AddContactBottomSheet extends StatelessWidget { AddContactBottomSheet({super.key}) { - controller.resetForm(); + controller.resetForm(); + nameController.clear(); - emailController.clear(); - phoneController.clear(); orgController.clear(); tagTextController.clear(); addressController.clear(); descriptionController.clear(); - // Reset labels - emailLabel.value = 'Office'; - phoneLabel.value = 'Work'; + emailControllers.add(TextEditingController()); + emailLabels.add('Office'.obs); + + phoneControllers.add(TextEditingController()); + phoneLabels.add('Work'.obs); } final controller = Get.put(AddContactController()); final formKey = GlobalKey(); - final emailLabel = 'Office'.obs; - final phoneLabel = 'Work'.obs; - final nameController = TextEditingController(); - final emailController = TextEditingController(); - final phoneController = TextEditingController(); final orgController = TextEditingController(); final tagTextController = TextEditingController(); final addressController = TextEditingController(); final descriptionController = TextEditingController(); + final RxList emailControllers = + [].obs; + final RxList emailLabels = [].obs; + + final RxList phoneControllers = + [].obs; + final RxList phoneLabels = [].obs; + InputDecoration _inputDecoration(String hint) => InputDecoration( hintText: hint, hintStyle: MyTextStyle.bodySmall(xMuted: true), @@ -52,7 +56,8 @@ class AddContactBottomSheet extends StatelessWidget { 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, ); @@ -67,7 +72,7 @@ class AddContactBottomSheet extends StatelessWidget { context: Navigator.of(Get.context!).overlay!.context, position: const RelativeRect.fromLTRB(100, 300, 100, 0), items: options - .map((e) => PopupMenuItem(value: e, child: Text(e))) + .map((e) => PopupMenuItem(value: e, child: Text(e))) .toList(), ); if (selected != null) selectedValue.value = selected; @@ -79,14 +84,117 @@ class AddContactBottomSheet extends StatelessWidget { readOnly: true, initialValue: selectedValue.value, style: const TextStyle(fontSize: 14), - decoration: _inputDecoration(hint) - .copyWith(suffixIcon: const Icon(Icons.expand_more)), + decoration: _inputDecoration(hint).copyWith( + suffixIcon: const Icon(Icons.expand_more), + ), ), ), ), )); } + Widget _buildLabeledRow( + String label, + RxString selectedLabel, + List options, + String inputLabel, + TextEditingController controller, + TextInputType inputType, { + VoidCallback? onRemove, + }) { + return Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.labelMedium(label), + MySpacing.height(8), + _popupSelector( + hint: "Label", + selectedValue: selectedLabel, + options: options), + ], + ), + ), + MySpacing.width(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.labelMedium(inputLabel), + MySpacing.height(8), + SizedBox( + height: 48, + child: TextFormField( + controller: controller, + keyboardType: inputType, + decoration: _inputDecoration("Enter $inputLabel"), + validator: (value) => value == null || value.trim().isEmpty + ? "$inputLabel is required" + : null, + ), + ), + ], + ), + ), + if (onRemove != null) + IconButton( + icon: const Icon(Icons.remove_circle_outline, color: Colors.red), + onPressed: onRemove, + ), + ], + ); + } + + Widget _buildEmailList() { + return Column( + children: List.generate(emailControllers.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 + ? () { + emailControllers.removeAt(index); + emailLabels.removeAt(index); + } + : null, + ), + ); + }), + ); + } + + Widget _buildPhoneList() { + return 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 _dropdownField({ required String label, required RxString selectedValue, @@ -166,9 +274,7 @@ class AddContactBottomSheet extends StatelessWidget { color: Colors.white, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(8), - boxShadow: const [ - BoxShadow(color: Colors.black12, blurRadius: 4, offset: Offset(0, 2)), - ], + boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 4)], ), child: ListView.builder( shrinkWrap: true, @@ -199,17 +305,12 @@ class AddContactBottomSheet extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return SingleChildScrollView( padding: MediaQuery.of(context).viewInsets, child: Container( decoration: BoxDecoration( - color: theme.cardColor, + color: Theme.of(context).cardColor, borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), - boxShadow: const [ - BoxShadow(color: Colors.black12, blurRadius: 12, offset: Offset(0, -2)) - ], ), child: Padding( padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), @@ -219,17 +320,8 @@ class AddContactBottomSheet extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( - child: Container( - width: 40, - height: 5, - decoration: BoxDecoration( - color: Colors.grey.shade300, - borderRadius: BorderRadius.circular(10), - ), - ), - ), - MySpacing.height(12), - Center(child: MyText.titleMedium("Create New Contact", fontWeight: 700)), + child: MyText.titleMedium("Create New Contact", + fontWeight: 700)), MySpacing.height(24), _sectionLabel("Basic Info"), MySpacing.height(16), @@ -239,11 +331,24 @@ class AddContactBottomSheet extends StatelessWidget { MySpacing.height(24), _sectionLabel("Contact Info"), MySpacing.height(16), - _buildLabeledRow("Email Label", emailLabel, ["Office", "Personal", "Other"], - "Email", emailController, TextInputType.emailAddress), - MySpacing.height(16), - _buildLabeledRow("Phone Label", phoneLabel, ["Work", "Mobile", "Other"], - "Phone", phoneController, TextInputType.phone), + 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), @@ -263,10 +368,6 @@ class AddContactBottomSheet extends StatelessWidget { options: controller.globalProjects, ), MySpacing.height(16), - MyText.labelMedium("Tags"), - MySpacing.height(8), - _tagInputSection(), - MySpacing.height(16), MyText.labelMedium("Select Bucket"), MySpacing.height(8), _dropdownField( @@ -275,9 +376,14 @@ class AddContactBottomSheet extends StatelessWidget { 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), + _buildTextField("Description", descriptionController, + maxLines: 2), MySpacing.height(24), _buildActionButtons(), ], @@ -299,8 +405,9 @@ class AddContactBottomSheet extends StatelessWidget { controller: controller, maxLines: maxLines, decoration: _inputDecoration("Enter $label"), - validator: (value) => - (value == null || value.trim().isEmpty) ? "$label is required" : null, + validator: (value) => value == null || value.trim().isEmpty + ? "$label is required" + : null, ), ], ); @@ -338,52 +445,6 @@ class AddContactBottomSheet extends StatelessWidget { ); } - Widget _buildLabeledRow( - String label, - RxString selectedLabel, - List options, - String inputLabel, - TextEditingController controller, - TextInputType inputType, - ) { - return Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.labelMedium(label), - MySpacing.height(8), - _popupSelector(hint: "Label", selectedValue: selectedLabel, options: options), - ], - ), - ), - MySpacing.width(12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.labelMedium(inputLabel), - MySpacing.height(8), - SizedBox( - height: 48, - child: TextFormField( - controller: controller, - keyboardType: inputType, - decoration: _inputDecoration("Enter $inputLabel"), - validator: (value) => - (value == null || value.trim().isEmpty) - ? "$inputLabel is required" - : null, - ), - ), - ], - ), - ), - ], - ); - } - Widget _buildActionButtons() { return Row( children: [ @@ -391,13 +452,15 @@ class AddContactBottomSheet extends StatelessWidget { child: OutlinedButton.icon( onPressed: () { Get.back(); - Get.delete(); // cleanup + Get.delete(); }, icon: const Icon(Icons.close, color: Colors.red), - label: MyText.bodyMedium("Cancel", color: Colors.red, fontWeight: 600), + label: + MyText.bodyMedium("Cancel", color: Colors.red, fontWeight: 600), style: OutlinedButton.styleFrom( side: const BorderSide(color: Colors.red), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), ), ), @@ -407,23 +470,43 @@ class AddContactBottomSheet extends StatelessWidget { 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( name: nameController.text.trim(), organization: orgController.text.trim(), - email: emailController.text.trim(), - emailLabel: emailLabel.value, - phone: phoneController.text.trim(), - phoneLabel: phoneLabel.value, + 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), + label: + MyText.bodyMedium("Save", color: Colors.white, fontWeight: 600), style: ElevatedButton.styleFrom( backgroundColor: Colors.indigo, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), ), ), diff --git a/lib/view/directory/contact_detail_screen.dart b/lib/view/directory/contact_detail_screen.dart index a84ad40..a507237 100644 --- a/lib/view/directory/contact_detail_screen.dart +++ b/lib/view/directory/contact_detail_screen.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:intl/intl.dart'; import 'package:flutter_html/flutter_html.dart' as html; import 'package:flutter_quill/flutter_quill.dart' as quill; import 'package:marco/controller/project_controller.dart'; @@ -14,6 +13,7 @@ import 'package:tab_indicator_styler/tab_indicator_styler.dart'; import 'package:marco/helpers/widgets/Directory/comment_editor_card.dart'; import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.dart'; import 'package:marco/model/directory/add_comment_bottom_sheet.dart'; +import 'package:marco/helpers/utils/date_time_utils.dart'; class ContactDetailScreen extends StatefulWidget { final ContactModel contact; @@ -233,8 +233,6 @@ class _ContactDetailScreenState extends State { ? widget.contact.contactPhones.first.phoneNumber : "-"; - final createdDate = DateTime.now(); - final formattedDate = DateFormat('MMMM dd, yyyy').format(createdDate); final tags = widget.contact.tags.map((e) => e.name).join(", "); final bucketNames = widget.contact.bucketIds @@ -268,7 +266,6 @@ class _ContactDetailScreenState extends State { onTap: () => LauncherUtils.launchPhone(phone), onLongPress: () => LauncherUtils.copyToClipboard(phone, typeLabel: "Phone")), - _iconInfoRow(Icons.calendar_today, "Created", formattedDate), _iconInfoRow(Icons.location_on, "Address", widget.contact.address), ]), _infoCard("Organization", [ @@ -381,8 +378,11 @@ class _ContactDetailScreenState extends State { color: Colors.indigo[700]), MySpacing.height(2), MyText.bodySmall( - DateFormat('dd MMM yyyy, hh:mm a') - .format(comment.createdAt), + DateTimeUtils.convertUtcToLocal( + comment.createdAt + .toString(), // pass as String + format: 'dd MMM yyyy, hh:mm a', + ), fontWeight: 500, color: Colors.grey[600], ), diff --git a/lib/view/directory/directory_main_screen.dart b/lib/view/directory/directory_main_screen.dart index cf99e07..fb3956b 100644 --- a/lib/view/directory/directory_main_screen.dart +++ b/lib/view/directory/directory_main_screen.dart @@ -29,7 +29,7 @@ class DirectoryMainScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.white, + backgroundColor: Colors.white, appBar: PreferredSize( preferredSize: const Size.fromHeight(72), child: AppBar( @@ -299,9 +299,6 @@ class DirectoryMainScreen extends StatelessWidget { final phone = contact.contactPhones.isNotEmpty ? contact.contactPhones.first.phoneNumber : '-'; - final email = contact.contactEmails.isNotEmpty - ? contact.contactEmails.first.emailAddress - : '-'; final nameParts = contact.name.trim().split(" "); final firstName = nameParts.first; final lastName = nameParts.length > 1 ? nameParts.last : ""; @@ -342,72 +339,84 @@ class DirectoryMainScreen extends StatelessWidget { MySpacing.height(6), // Launcher Row - Wrap( - spacing: 12, - runSpacing: 6, + Column( + crossAxisAlignment: + CrossAxisAlignment.start, children: [ - if (email != '-') - GestureDetector( - onTap: () => - LauncherUtils.launchEmail(email), - onLongPress: () => - LauncherUtils.copyToClipboard( - email, - typeLabel: 'Email'), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.email_outlined, - size: 16, - color: Colors.indigo), - MySpacing.width(4), - ConstrainedBox( - constraints: - const BoxConstraints( - maxWidth: 120), - child: MyText.labelSmall( - email, - overflow: - TextOverflow.ellipsis, - color: Colors.indigo, - decoration: - TextDecoration.underline, - ), + ...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( + mainAxisSize: MainAxisSize.min, + 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, + ), + ), + ], ), - ], - ), - ), - if (phone != '-') - GestureDetector( - onTap: () => - LauncherUtils.launchPhone(phone), - onLongPress: () => - LauncherUtils.copyToClipboard( - phone, - typeLabel: 'Phone number'), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.phone_outlined, - size: 16, - color: Colors.indigo), - MySpacing.width(4), - ConstrainedBox( - constraints: - const BoxConstraints( - maxWidth: 100), - child: MyText.labelSmall( - phone, - overflow: - TextOverflow.ellipsis, - color: Colors.indigo, - decoration: - TextDecoration.underline, - ), + ), + )), + ...contact.contactPhones.map((p) => + GestureDetector( + onTap: () => + LauncherUtils.launchPhone( + p.phoneNumber), + onLongPress: () => + LauncherUtils.copyToClipboard( + p.phoneNumber, + typeLabel: 'Phone number'), + child: Padding( + padding: const EdgeInsets.only( + bottom: 4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.phone_outlined, + size: 16, + color: Colors.indigo), + MySpacing.width(4), + ConstrainedBox( + constraints: + const BoxConstraints( + maxWidth: 160), + child: MyText.labelSmall( + p.phoneNumber, + overflow: + TextOverflow.ellipsis, + color: Colors.indigo, + decoration: TextDecoration + .underline, + ), + ), + ], ), - ], - ), - ), + ), + )), ], ), @@ -423,7 +432,6 @@ class DirectoryMainScreen extends StatelessWidget { ], ), ), - // WhatsApp launcher icon Column( mainAxisAlignment: MainAxisAlignment.center,