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

View File

@ -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 [];
} }

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 { 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),
), ),
), ),

View File

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

View File

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