feat(directory): enhance AddContact functionality to support multiple emails and phones, improve logging, and refactor contact detail display

This commit is contained in:
Vaibhav Surve 2025-07-05 13:19:53 +05:30
parent 62c49b5429
commit e7940941ed
6 changed files with 369 additions and 230 deletions

View File

@ -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<void> submitContact({
required String name,
required String organization,
required String email,
required String emailLabel,
required String phone,
required String phoneLabel,
required List<Map<String, String>> emails,
required List<Map<String, String>> 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<void> 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<String>.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);
}
}

View File

@ -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<bool> createContact(Map<String, dynamic> 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<List<String>> 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<String>.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 [];
}

View File

@ -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);
}
}

View File

@ -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<FormState>();
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<TextEditingController> emailControllers =
<TextEditingController>[].obs;
final RxList<RxString> emailLabels = <RxString>[].obs;
final RxList<TextEditingController> phoneControllers =
<TextEditingController>[].obs;
final RxList<RxString> phoneLabels = <RxString>[].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<String>(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<String> 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<String> 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<AddContactController>(); // cleanup
Get.delete<AddContactController>();
},
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),
),
),

View File

@ -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<ContactDetailScreen> {
? 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<ContactDetailScreen> {
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<ContactDetailScreen> {
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],
),

View File

@ -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,