feat(directory): enhance AddContact functionality to support multiple emails and phones, improve logging, and refactor contact detail display
This commit is contained in:
parent
62c49b5429
commit
e7940941ed
@ -1,3 +1,5 @@
|
|||||||
|
// Updated AddContactController to support multiple emails and phones
|
||||||
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
@ -62,10 +64,10 @@ class AddContactController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
buckets.assignAll(names);
|
buckets.assignAll(names);
|
||||||
logSafe("Fetched ${names.length} buckets");
|
logSafe("Fetched \${names.length} buckets");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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 {
|
try {
|
||||||
final orgs = await ApiService.getOrganizationList();
|
final orgs = await ApiService.getOrganizationList();
|
||||||
organizationNames.assignAll(orgs);
|
organizationNames.assignAll(orgs);
|
||||||
logSafe("Fetched ${orgs.length} organization names");
|
logSafe("Fetched \${orgs.length} organization names");
|
||||||
} catch (e) {
|
} 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({
|
Future<void> submitContact({
|
||||||
required String name,
|
required String name,
|
||||||
required String organization,
|
required String organization,
|
||||||
required String email,
|
required List<Map<String, String>> emails,
|
||||||
required String emailLabel,
|
required List<Map<String, String>> phones,
|
||||||
required String phone,
|
|
||||||
required String phoneLabel,
|
|
||||||
required String address,
|
required String address,
|
||||||
required String description,
|
required String description,
|
||||||
}) async {
|
}) async {
|
||||||
@ -96,9 +96,7 @@ class AddContactController extends GetxController {
|
|||||||
|
|
||||||
final tagObjects = enteredTags.map((tagName) {
|
final tagObjects = enteredTags.map((tagName) {
|
||||||
final tagId = tagsMap[tagName];
|
final tagId = tagsMap[tagName];
|
||||||
return tagId != null
|
return tagId != null ? {"id": tagId, "name": tagName} : {"name": tagName};
|
||||||
? {"id": tagId, "name": tagName}
|
|
||||||
: {"name": tagName};
|
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
final body = {
|
final body = {
|
||||||
@ -108,18 +106,8 @@ class AddContactController extends GetxController {
|
|||||||
"projectIds": projectId != null ? [projectId] : [],
|
"projectIds": projectId != null ? [projectId] : [],
|
||||||
"bucketIds": bucketId != null ? [bucketId] : [],
|
"bucketIds": bucketId != null ? [bucketId] : [],
|
||||||
"tags": tagObjects,
|
"tags": tagObjects,
|
||||||
"contactEmails": [
|
"contactEmails": emails,
|
||||||
{
|
"contactPhones": phones,
|
||||||
"label": emailLabel,
|
|
||||||
"emailAddress": email,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"contactPhones": [
|
|
||||||
{
|
|
||||||
"label": phoneLabel,
|
|
||||||
"phoneNumber": phone,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"address": address,
|
"address": address,
|
||||||
"description": description,
|
"description": description,
|
||||||
};
|
};
|
||||||
@ -129,10 +117,7 @@ class AddContactController extends GetxController {
|
|||||||
final response = await ApiService.createContact(body);
|
final response = await ApiService.createContact(body);
|
||||||
if (response == true) {
|
if (response == true) {
|
||||||
logSafe("Contact creation succeeded");
|
logSafe("Contact creation succeeded");
|
||||||
|
|
||||||
// Send result back to previous screen
|
|
||||||
Get.back(result: true);
|
Get.back(result: true);
|
||||||
|
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Success",
|
title: "Success",
|
||||||
message: "Contact created successfully",
|
message: "Contact created successfully",
|
||||||
@ -147,7 +132,7 @@ class AddContactController extends GetxController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logSafe("Contact creation error: $e", level: LogLevel.error);
|
logSafe("Contact creation error: \$e", level: LogLevel.error);
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: "Something went wrong",
|
message: "Something went wrong",
|
||||||
@ -164,12 +149,9 @@ class AddContactController extends GetxController {
|
|||||||
|
|
||||||
final lower = query.toLowerCase();
|
final lower = query.toLowerCase();
|
||||||
filteredOrgSuggestions.assignAll(
|
filteredOrgSuggestions.assignAll(
|
||||||
organizationNames
|
organizationNames.where((name) => name.toLowerCase().contains(lower)).toList(),
|
||||||
.where((name) => name.toLowerCase().contains(lower))
|
|
||||||
.toList(),
|
|
||||||
);
|
);
|
||||||
logSafe("Filtered organization suggestions for: $query",
|
logSafe("Filtered organization suggestions for: \$query", level: LogLevel.debug);
|
||||||
level: LogLevel.debug);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchGlobalProjects() async {
|
Future<void> fetchGlobalProjects() async {
|
||||||
@ -186,10 +168,10 @@ class AddContactController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
globalProjects.assignAll(names);
|
globalProjects.assignAll(names);
|
||||||
logSafe("Fetched ${names.length} global projects");
|
logSafe("Fetched \${names.length} global projects");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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(
|
tags.assignAll(List<String>.from(
|
||||||
response['data'].map((e) => e['name'] ?? '').where((e) => e != ''),
|
response['data'].map((e) => e['name'] ?? '').where((e) => e != ''),
|
||||||
));
|
));
|
||||||
logSafe("Fetched ${tags.length} tags");
|
logSafe("Fetched \${tags.length} tags");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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();
|
final lower = query.toLowerCase();
|
||||||
filteredSuggestions.assignAll(
|
filteredSuggestions.assignAll(
|
||||||
tags
|
tags.where((tag) => tag.toLowerCase().contains(lower) && !enteredTags.contains(tag)).toList(),
|
||||||
.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() {
|
void clearSuggestions() {
|
||||||
@ -242,22 +221,22 @@ class AddContactController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
categories.assignAll(names);
|
categories.assignAll(names);
|
||||||
logSafe("Fetched ${names.length} contact categories");
|
logSafe("Fetched \${names.length} contact categories");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logSafe("Failed to fetch categories: $e", level: LogLevel.error);
|
logSafe("Failed to fetch categories: \$e", level: LogLevel.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void addEnteredTag(String tag) {
|
void addEnteredTag(String tag) {
|
||||||
if (tag.trim().isNotEmpty && !enteredTags.contains(tag.trim())) {
|
if (tag.trim().isNotEmpty && !enteredTags.contains(tag.trim())) {
|
||||||
enteredTags.add(tag.trim());
|
enteredTags.add(tag.trim());
|
||||||
logSafe("Added tag: $tag", level: LogLevel.debug);
|
logSafe("Added tag: \$tag", level: LogLevel.debug);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeEnteredTag(String tag) {
|
void removeEnteredTag(String tag) {
|
||||||
enteredTags.remove(tag);
|
enteredTags.remove(tag);
|
||||||
logSafe("Removed tag: $tag", level: LogLevel.debug);
|
logSafe("Removed tag: \$tag", level: LogLevel.debug);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,23 +106,45 @@ class ApiService {
|
|||||||
bool hasRetried = false,
|
bool hasRetried = false,
|
||||||
}) async {
|
}) async {
|
||||||
String? token = await _getToken();
|
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")
|
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint")
|
||||||
.replace(queryParameters: queryParams);
|
.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 {
|
try {
|
||||||
final response =
|
final response =
|
||||||
await http.get(uri, headers: _headers(token)).timeout(timeout);
|
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) {
|
if (response.statusCode == 401 && !hasRetried) {
|
||||||
logSafe("Unauthorized. Attempting token refresh...");
|
logSafe("Unauthorized (401). Attempting token refresh...",
|
||||||
|
level: LogLevel.warning);
|
||||||
|
|
||||||
if (await AuthService.refreshToken()) {
|
if (await AuthService.refreshToken()) {
|
||||||
return await _getRequest(endpoint,
|
logSafe("Token refresh succeeded. Retrying request...",
|
||||||
queryParams: queryParams, hasRetried: true);
|
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;
|
return response;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logSafe("HTTP GET Exception: $e", level: LogLevel.error);
|
logSafe("HTTP GET Exception: $e", level: LogLevel.error);
|
||||||
@ -324,7 +346,7 @@ class ApiService {
|
|||||||
|
|
||||||
static Future<bool> createContact(Map<String, dynamic> payload) async {
|
static Future<bool> createContact(Map<String, dynamic> payload) async {
|
||||||
try {
|
try {
|
||||||
logSafe("Submitting contact payload: $payload", sensitive: true);
|
logSafe("Submitting contact payload: $payload");
|
||||||
|
|
||||||
final response = await _postRequest(ApiEndpoints.createContact, payload);
|
final response = await _postRequest(ApiEndpoints.createContact, payload);
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
@ -345,15 +367,24 @@ class ApiService {
|
|||||||
|
|
||||||
static Future<List<String>> getOrganizationList() async {
|
static Future<List<String>> getOrganizationList() async {
|
||||||
try {
|
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) {
|
if (response != null && response.statusCode == 200) {
|
||||||
final body = jsonDecode(response.body);
|
final body = jsonDecode(response.body);
|
||||||
if (body['success'] == true && body['data'] is List) {
|
if (body['success'] == true && body['data'] is List) {
|
||||||
return List<String>.from(body['data']);
|
return List<String>.from(body['data']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e, stackTrace) {
|
||||||
logSafe("Failed to fetch organization names: $e", level: LogLevel.error);
|
logSafe("Failed to fetch organization names: $e", level: LogLevel.error);
|
||||||
|
logSafe("Stack trace: $stackTrace", level: LogLevel.debug);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
38
lib/helpers/utils/date_time_utils.dart
Normal file
38
lib/helpers/utils/date_time_utils.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -7,34 +7,38 @@ import 'package:marco/helpers/widgets/my_text_style.dart';
|
|||||||
|
|
||||||
class AddContactBottomSheet extends StatelessWidget {
|
class AddContactBottomSheet extends StatelessWidget {
|
||||||
AddContactBottomSheet({super.key}) {
|
AddContactBottomSheet({super.key}) {
|
||||||
controller.resetForm();
|
controller.resetForm();
|
||||||
|
|
||||||
nameController.clear();
|
nameController.clear();
|
||||||
emailController.clear();
|
|
||||||
phoneController.clear();
|
|
||||||
orgController.clear();
|
orgController.clear();
|
||||||
tagTextController.clear();
|
tagTextController.clear();
|
||||||
addressController.clear();
|
addressController.clear();
|
||||||
descriptionController.clear();
|
descriptionController.clear();
|
||||||
|
|
||||||
// Reset labels
|
emailControllers.add(TextEditingController());
|
||||||
emailLabel.value = 'Office';
|
emailLabels.add('Office'.obs);
|
||||||
phoneLabel.value = 'Work';
|
|
||||||
|
phoneControllers.add(TextEditingController());
|
||||||
|
phoneLabels.add('Work'.obs);
|
||||||
}
|
}
|
||||||
|
|
||||||
final controller = Get.put(AddContactController());
|
final controller = Get.put(AddContactController());
|
||||||
final formKey = GlobalKey<FormState>();
|
final formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
final emailLabel = 'Office'.obs;
|
|
||||||
final phoneLabel = 'Work'.obs;
|
|
||||||
|
|
||||||
final nameController = TextEditingController();
|
final nameController = TextEditingController();
|
||||||
final emailController = TextEditingController();
|
|
||||||
final phoneController = TextEditingController();
|
|
||||||
final orgController = TextEditingController();
|
final orgController = TextEditingController();
|
||||||
final tagTextController = TextEditingController();
|
final tagTextController = TextEditingController();
|
||||||
final addressController = TextEditingController();
|
final addressController = TextEditingController();
|
||||||
final descriptionController = 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(
|
InputDecoration _inputDecoration(String hint) => InputDecoration(
|
||||||
hintText: hint,
|
hintText: hint,
|
||||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
||||||
@ -52,7 +56,8 @@ class AddContactBottomSheet extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: const BorderSide(color: Colors.blueAccent, width: 1.5),
|
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,
|
isDense: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -67,7 +72,7 @@ class AddContactBottomSheet extends StatelessWidget {
|
|||||||
context: Navigator.of(Get.context!).overlay!.context,
|
context: Navigator.of(Get.context!).overlay!.context,
|
||||||
position: const RelativeRect.fromLTRB(100, 300, 100, 0),
|
position: const RelativeRect.fromLTRB(100, 300, 100, 0),
|
||||||
items: options
|
items: options
|
||||||
.map((e) => PopupMenuItem<String>(value: e, child: Text(e)))
|
.map((e) => PopupMenuItem(value: e, child: Text(e)))
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
if (selected != null) selectedValue.value = selected;
|
if (selected != null) selectedValue.value = selected;
|
||||||
@ -79,14 +84,117 @@ class AddContactBottomSheet extends StatelessWidget {
|
|||||||
readOnly: true,
|
readOnly: true,
|
||||||
initialValue: selectedValue.value,
|
initialValue: selectedValue.value,
|
||||||
style: const TextStyle(fontSize: 14),
|
style: const TextStyle(fontSize: 14),
|
||||||
decoration: _inputDecoration(hint)
|
decoration: _inputDecoration(hint).copyWith(
|
||||||
.copyWith(suffixIcon: const Icon(Icons.expand_more)),
|
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({
|
Widget _dropdownField({
|
||||||
required String label,
|
required String label,
|
||||||
required RxString selectedValue,
|
required RxString selectedValue,
|
||||||
@ -166,9 +274,7 @@ class AddContactBottomSheet extends StatelessWidget {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
boxShadow: const [
|
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 4)],
|
||||||
BoxShadow(color: Colors.black12, blurRadius: 4, offset: Offset(0, 2)),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
@ -199,17 +305,12 @@ class AddContactBottomSheet extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
padding: MediaQuery.of(context).viewInsets,
|
padding: MediaQuery.of(context).viewInsets,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.cardColor,
|
color: Theme.of(context).cardColor,
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||||
boxShadow: const [
|
|
||||||
BoxShadow(color: Colors.black12, blurRadius: 12, offset: Offset(0, -2))
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
|
||||||
@ -219,17 +320,8 @@ class AddContactBottomSheet extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Center(
|
Center(
|
||||||
child: Container(
|
child: MyText.titleMedium("Create New Contact",
|
||||||
width: 40,
|
fontWeight: 700)),
|
||||||
height: 5,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade300,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.height(12),
|
|
||||||
Center(child: MyText.titleMedium("Create New Contact", fontWeight: 700)),
|
|
||||||
MySpacing.height(24),
|
MySpacing.height(24),
|
||||||
_sectionLabel("Basic Info"),
|
_sectionLabel("Basic Info"),
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
@ -239,11 +331,24 @@ class AddContactBottomSheet extends StatelessWidget {
|
|||||||
MySpacing.height(24),
|
MySpacing.height(24),
|
||||||
_sectionLabel("Contact Info"),
|
_sectionLabel("Contact Info"),
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
_buildLabeledRow("Email Label", emailLabel, ["Office", "Personal", "Other"],
|
Obx(() => _buildEmailList()),
|
||||||
"Email", emailController, TextInputType.emailAddress),
|
TextButton.icon(
|
||||||
MySpacing.height(16),
|
onPressed: () {
|
||||||
_buildLabeledRow("Phone Label", phoneLabel, ["Work", "Mobile", "Other"],
|
emailControllers.add(TextEditingController());
|
||||||
"Phone", phoneController, TextInputType.phone),
|
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),
|
MySpacing.height(24),
|
||||||
_sectionLabel("Other Details"),
|
_sectionLabel("Other Details"),
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
@ -263,10 +368,6 @@ class AddContactBottomSheet extends StatelessWidget {
|
|||||||
options: controller.globalProjects,
|
options: controller.globalProjects,
|
||||||
),
|
),
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
MyText.labelMedium("Tags"),
|
|
||||||
MySpacing.height(8),
|
|
||||||
_tagInputSection(),
|
|
||||||
MySpacing.height(16),
|
|
||||||
MyText.labelMedium("Select Bucket"),
|
MyText.labelMedium("Select Bucket"),
|
||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
_dropdownField(
|
_dropdownField(
|
||||||
@ -275,9 +376,14 @@ class AddContactBottomSheet extends StatelessWidget {
|
|||||||
options: controller.buckets,
|
options: controller.buckets,
|
||||||
),
|
),
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
|
MyText.labelMedium("Tags"),
|
||||||
|
MySpacing.height(8),
|
||||||
|
_tagInputSection(),
|
||||||
|
MySpacing.height(16),
|
||||||
_buildTextField("Address", addressController, maxLines: 2),
|
_buildTextField("Address", addressController, maxLines: 2),
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
_buildTextField("Description", descriptionController, maxLines: 2),
|
_buildTextField("Description", descriptionController,
|
||||||
|
maxLines: 2),
|
||||||
MySpacing.height(24),
|
MySpacing.height(24),
|
||||||
_buildActionButtons(),
|
_buildActionButtons(),
|
||||||
],
|
],
|
||||||
@ -299,8 +405,9 @@ class AddContactBottomSheet extends StatelessWidget {
|
|||||||
controller: controller,
|
controller: controller,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
decoration: _inputDecoration("Enter $label"),
|
decoration: _inputDecoration("Enter $label"),
|
||||||
validator: (value) =>
|
validator: (value) => value == null || value.trim().isEmpty
|
||||||
(value == null || value.trim().isEmpty) ? "$label is required" : null,
|
? "$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() {
|
Widget _buildActionButtons() {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
@ -391,13 +452,15 @@ class AddContactBottomSheet extends StatelessWidget {
|
|||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Get.back();
|
Get.back();
|
||||||
Get.delete<AddContactController>(); // cleanup
|
Get.delete<AddContactController>();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.close, color: Colors.red),
|
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(
|
style: OutlinedButton.styleFrom(
|
||||||
side: const BorderSide(color: Colors.red),
|
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),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -407,23 +470,43 @@ class AddContactBottomSheet extends StatelessWidget {
|
|||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (formKey.currentState!.validate()) {
|
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(
|
controller.submitContact(
|
||||||
name: nameController.text.trim(),
|
name: nameController.text.trim(),
|
||||||
organization: orgController.text.trim(),
|
organization: orgController.text.trim(),
|
||||||
email: emailController.text.trim(),
|
emails: emails,
|
||||||
emailLabel: emailLabel.value,
|
phones: phones,
|
||||||
phone: phoneController.text.trim(),
|
|
||||||
phoneLabel: phoneLabel.value,
|
|
||||||
address: addressController.text.trim(),
|
address: addressController.text.trim(),
|
||||||
description: descriptionController.text.trim(),
|
description: descriptionController.text.trim(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.check_circle_outline, color: Colors.white),
|
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(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.indigo,
|
backgroundColor: Colors.indigo,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:flutter_html/flutter_html.dart' as html;
|
import 'package:flutter_html/flutter_html.dart' as html;
|
||||||
import 'package:flutter_quill/flutter_quill.dart' as quill;
|
import 'package:flutter_quill/flutter_quill.dart' as quill;
|
||||||
import 'package:marco/controller/project_controller.dart';
|
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:marco/helpers/widgets/Directory/comment_editor_card.dart';
|
||||||
import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.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/model/directory/add_comment_bottom_sheet.dart';
|
||||||
|
import 'package:marco/helpers/utils/date_time_utils.dart';
|
||||||
|
|
||||||
class ContactDetailScreen extends StatefulWidget {
|
class ContactDetailScreen extends StatefulWidget {
|
||||||
final ContactModel contact;
|
final ContactModel contact;
|
||||||
@ -233,8 +233,6 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
? widget.contact.contactPhones.first.phoneNumber
|
? 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 tags = widget.contact.tags.map((e) => e.name).join(", ");
|
||||||
|
|
||||||
final bucketNames = widget.contact.bucketIds
|
final bucketNames = widget.contact.bucketIds
|
||||||
@ -268,7 +266,6 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
onTap: () => LauncherUtils.launchPhone(phone),
|
onTap: () => LauncherUtils.launchPhone(phone),
|
||||||
onLongPress: () =>
|
onLongPress: () =>
|
||||||
LauncherUtils.copyToClipboard(phone, typeLabel: "Phone")),
|
LauncherUtils.copyToClipboard(phone, typeLabel: "Phone")),
|
||||||
_iconInfoRow(Icons.calendar_today, "Created", formattedDate),
|
|
||||||
_iconInfoRow(Icons.location_on, "Address", widget.contact.address),
|
_iconInfoRow(Icons.location_on, "Address", widget.contact.address),
|
||||||
]),
|
]),
|
||||||
_infoCard("Organization", [
|
_infoCard("Organization", [
|
||||||
@ -381,8 +378,11 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
color: Colors.indigo[700]),
|
color: Colors.indigo[700]),
|
||||||
MySpacing.height(2),
|
MySpacing.height(2),
|
||||||
MyText.bodySmall(
|
MyText.bodySmall(
|
||||||
DateFormat('dd MMM yyyy, hh:mm a')
|
DateTimeUtils.convertUtcToLocal(
|
||||||
.format(comment.createdAt),
|
comment.createdAt
|
||||||
|
.toString(), // pass as String
|
||||||
|
format: 'dd MMM yyyy, hh:mm a',
|
||||||
|
),
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
),
|
),
|
||||||
|
@ -29,7 +29,7 @@ class DirectoryMainScreen extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
appBar: PreferredSize(
|
appBar: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(72),
|
preferredSize: const Size.fromHeight(72),
|
||||||
child: AppBar(
|
child: AppBar(
|
||||||
@ -299,9 +299,6 @@ class DirectoryMainScreen extends StatelessWidget {
|
|||||||
final phone = contact.contactPhones.isNotEmpty
|
final phone = contact.contactPhones.isNotEmpty
|
||||||
? contact.contactPhones.first.phoneNumber
|
? contact.contactPhones.first.phoneNumber
|
||||||
: '-';
|
: '-';
|
||||||
final email = contact.contactEmails.isNotEmpty
|
|
||||||
? contact.contactEmails.first.emailAddress
|
|
||||||
: '-';
|
|
||||||
final nameParts = contact.name.trim().split(" ");
|
final nameParts = contact.name.trim().split(" ");
|
||||||
final firstName = nameParts.first;
|
final firstName = nameParts.first;
|
||||||
final lastName = nameParts.length > 1 ? nameParts.last : "";
|
final lastName = nameParts.length > 1 ? nameParts.last : "";
|
||||||
@ -342,72 +339,84 @@ class DirectoryMainScreen extends StatelessWidget {
|
|||||||
MySpacing.height(6),
|
MySpacing.height(6),
|
||||||
|
|
||||||
// Launcher Row
|
// Launcher Row
|
||||||
Wrap(
|
Column(
|
||||||
spacing: 12,
|
crossAxisAlignment:
|
||||||
runSpacing: 6,
|
CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (email != '-')
|
...contact.contactEmails.map((e) =>
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
LauncherUtils.launchEmail(email),
|
LauncherUtils.launchEmail(
|
||||||
onLongPress: () =>
|
e.emailAddress),
|
||||||
LauncherUtils.copyToClipboard(
|
onLongPress: () =>
|
||||||
email,
|
LauncherUtils.copyToClipboard(
|
||||||
typeLabel: 'Email'),
|
e.emailAddress,
|
||||||
child: Row(
|
typeLabel: 'Email'),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Padding(
|
||||||
children: [
|
padding: const EdgeInsets.only(
|
||||||
const Icon(Icons.email_outlined,
|
bottom: 4),
|
||||||
size: 16,
|
child: Row(
|
||||||
color: Colors.indigo),
|
mainAxisSize: MainAxisSize.min,
|
||||||
MySpacing.width(4),
|
children: [
|
||||||
ConstrainedBox(
|
const Icon(
|
||||||
constraints:
|
Icons.email_outlined,
|
||||||
const BoxConstraints(
|
size: 16,
|
||||||
maxWidth: 120),
|
color: Colors.indigo),
|
||||||
child: MyText.labelSmall(
|
MySpacing.width(4),
|
||||||
email,
|
ConstrainedBox(
|
||||||
overflow:
|
constraints:
|
||||||
TextOverflow.ellipsis,
|
const BoxConstraints(
|
||||||
color: Colors.indigo,
|
maxWidth: 180),
|
||||||
decoration:
|
child: MyText.labelSmall(
|
||||||
TextDecoration.underline,
|
e.emailAddress,
|
||||||
),
|
overflow:
|
||||||
|
TextOverflow.ellipsis,
|
||||||
|
color: Colors.indigo,
|
||||||
|
decoration: TextDecoration
|
||||||
|
.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
)),
|
||||||
),
|
...contact.contactPhones.map((p) =>
|
||||||
if (phone != '-')
|
GestureDetector(
|
||||||
GestureDetector(
|
onTap: () =>
|
||||||
onTap: () =>
|
LauncherUtils.launchPhone(
|
||||||
LauncherUtils.launchPhone(phone),
|
p.phoneNumber),
|
||||||
onLongPress: () =>
|
onLongPress: () =>
|
||||||
LauncherUtils.copyToClipboard(
|
LauncherUtils.copyToClipboard(
|
||||||
phone,
|
p.phoneNumber,
|
||||||
typeLabel: 'Phone number'),
|
typeLabel: 'Phone number'),
|
||||||
child: Row(
|
child: Padding(
|
||||||
mainAxisSize: MainAxisSize.min,
|
padding: const EdgeInsets.only(
|
||||||
children: [
|
bottom: 4),
|
||||||
const Icon(Icons.phone_outlined,
|
child: Row(
|
||||||
size: 16,
|
mainAxisSize: MainAxisSize.min,
|
||||||
color: Colors.indigo),
|
children: [
|
||||||
MySpacing.width(4),
|
const Icon(
|
||||||
ConstrainedBox(
|
Icons.phone_outlined,
|
||||||
constraints:
|
size: 16,
|
||||||
const BoxConstraints(
|
color: Colors.indigo),
|
||||||
maxWidth: 100),
|
MySpacing.width(4),
|
||||||
child: MyText.labelSmall(
|
ConstrainedBox(
|
||||||
phone,
|
constraints:
|
||||||
overflow:
|
const BoxConstraints(
|
||||||
TextOverflow.ellipsis,
|
maxWidth: 160),
|
||||||
color: Colors.indigo,
|
child: MyText.labelSmall(
|
||||||
decoration:
|
p.phoneNumber,
|
||||||
TextDecoration.underline,
|
overflow:
|
||||||
),
|
TextOverflow.ellipsis,
|
||||||
|
color: Colors.indigo,
|
||||||
|
decoration: TextDecoration
|
||||||
|
.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
)),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
@ -423,7 +432,6 @@ class DirectoryMainScreen extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// WhatsApp launcher icon
|
// WhatsApp launcher icon
|
||||||
Column(
|
Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user