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: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);
|
||||
}
|
||||
}
|
||||
|
@ -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 [];
|
||||
}
|
||||
|
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 {
|
||||
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),
|
||||
),
|
||||
),
|
||||
|
@ -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],
|
||||
),
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user