Refactor ContactDetailScreen and DirectoryView for improved readability and performance
- Moved the Delta to HTML conversion logic outside of the ContactDetailScreen class for better separation of concerns. - Simplified the handling of email and phone display in DirectoryView to show only the first entry, reducing redundancy. - Enhanced the layout and structure of the ContactDetailScreen for better maintainability. - Introduced a new ProjectLabel widget to encapsulate project display logic in the ContactDetailScreen. - Cleaned up unnecessary comments and improved code formatting for consistency.
This commit is contained in:
parent
5bc811f91f
commit
ddbc1ec1e5
@ -18,89 +18,75 @@ class AddContactBottomSheet extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
final controller = Get.put(AddContactController());
|
||||
// Controllers and state
|
||||
final AddContactController controller = Get.put(AddContactController());
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
final nameController = TextEditingController();
|
||||
final orgController = TextEditingController();
|
||||
final addressController = TextEditingController();
|
||||
final descriptionController = TextEditingController();
|
||||
final tagTextController = TextEditingController();
|
||||
final RxBool showAdvanced = false.obs;
|
||||
final RxList<TextEditingController> emailControllers =
|
||||
<TextEditingController>[].obs;
|
||||
final RxList<RxString> emailLabels = <RxString>[].obs;
|
||||
|
||||
final RxList<TextEditingController> phoneControllers =
|
||||
<TextEditingController>[].obs;
|
||||
final RxList<RxString> phoneLabels = <RxString>[].obs;
|
||||
// Use Rx for advanced toggle and dynamic fields
|
||||
final showAdvanced = false.obs;
|
||||
final emailControllers = <TextEditingController>[].obs;
|
||||
final emailLabels = <RxString>[].obs;
|
||||
final phoneControllers = <TextEditingController>[].obs;
|
||||
final phoneLabels = <RxString>[].obs;
|
||||
|
||||
// For required bucket validation (new)
|
||||
final bucketError = ''.obs;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller.resetForm();
|
||||
_initFields();
|
||||
}
|
||||
|
||||
nameController.text = widget.existingContact?.name ?? '';
|
||||
orgController.text = widget.existingContact?.organization ?? '';
|
||||
addressController.text = widget.existingContact?.address ?? '';
|
||||
descriptionController.text = widget.existingContact?.description ?? '';
|
||||
tagTextController.clear();
|
||||
|
||||
if (widget.existingContact != null) {
|
||||
emailControllers.clear();
|
||||
emailLabels.clear();
|
||||
for (var email in widget.existingContact!.contactEmails) {
|
||||
emailControllers.add(TextEditingController(text: email.emailAddress));
|
||||
emailLabels.add((email.label).obs);
|
||||
}
|
||||
if (emailControllers.isEmpty) {
|
||||
emailControllers.add(TextEditingController());
|
||||
emailLabels.add('Office'.obs);
|
||||
}
|
||||
|
||||
phoneControllers.clear();
|
||||
phoneLabels.clear();
|
||||
for (var phone in widget.existingContact!.contactPhones) {
|
||||
phoneControllers.add(TextEditingController(text: phone.phoneNumber));
|
||||
phoneLabels.add((phone.label).obs);
|
||||
}
|
||||
if (phoneControllers.isEmpty) {
|
||||
phoneControllers.add(TextEditingController());
|
||||
phoneLabels.add('Work'.obs);
|
||||
}
|
||||
|
||||
controller.enteredTags.assignAll(
|
||||
widget.existingContact!.tags.map((tag) => tag.name).toList(),
|
||||
);
|
||||
|
||||
void _initFields() {
|
||||
final c = widget.existingContact;
|
||||
if (c != null) {
|
||||
nameController.text = c.name;
|
||||
orgController.text = c.organization;
|
||||
addressController.text = c.address;
|
||||
descriptionController.text = c.description ;
|
||||
}
|
||||
if (c != null) {
|
||||
emailControllers.assignAll(c.contactEmails.isEmpty
|
||||
? [TextEditingController()]
|
||||
: c.contactEmails.map((e) => TextEditingController(text: e.emailAddress)));
|
||||
emailLabels.assignAll(c.contactEmails.isEmpty
|
||||
? ['Office'.obs]
|
||||
: c.contactEmails.map((e) => e.label.obs));
|
||||
phoneControllers.assignAll(c.contactPhones.isEmpty
|
||||
? [TextEditingController()]
|
||||
: c.contactPhones.map((p) => TextEditingController(text: p.phoneNumber)));
|
||||
phoneLabels.assignAll(c.contactPhones.isEmpty
|
||||
? ['Work'.obs]
|
||||
: c.contactPhones.map((p) => p.label.obs));
|
||||
controller.enteredTags.assignAll(c.tags.map((tag) => tag.name));
|
||||
ever(controller.isInitialized, (bool ready) {
|
||||
if (ready) {
|
||||
final projectIds = widget.existingContact!.projectIds;
|
||||
final bucketId = widget.existingContact!.bucketIds.firstOrNull;
|
||||
final categoryName = widget.existingContact!.contactCategory?.name;
|
||||
|
||||
if (categoryName != null) {
|
||||
controller.selectedCategory.value = categoryName;
|
||||
}
|
||||
|
||||
final projectIds = c.projectIds;
|
||||
final bucketId = c.bucketIds.firstOrNull;
|
||||
final categoryName = c.contactCategory?.name;
|
||||
if (categoryName != null) controller.selectedCategory.value = categoryName;
|
||||
if (projectIds != null) {
|
||||
final names = projectIds
|
||||
.map((id) {
|
||||
return controller.projectsMap.entries
|
||||
controller.selectedProjects.assignAll(
|
||||
projectIds //
|
||||
.map((id) => controller.projectsMap.entries
|
||||
.firstWhereOrNull((e) => e.value == id)
|
||||
?.key;
|
||||
})
|
||||
.whereType<String>()
|
||||
.toList();
|
||||
controller.selectedProjects.assignAll(names);
|
||||
?.key)
|
||||
.whereType<String>()
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
if (bucketId != null) {
|
||||
final name = controller.bucketsMap.entries
|
||||
.firstWhereOrNull((e) => e.value == bucketId)
|
||||
?.key;
|
||||
if (name != null) {
|
||||
controller.selectedBucket.value = name;
|
||||
}
|
||||
if (name != null) controller.selectedBucket.value = name;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -110,6 +96,7 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
phoneControllers.add(TextEditingController());
|
||||
phoneLabels.add('Work'.obs);
|
||||
}
|
||||
tagTextController.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -119,12 +106,17 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
tagTextController.dispose();
|
||||
addressController.dispose();
|
||||
descriptionController.dispose();
|
||||
emailControllers.forEach((e) => e.dispose());
|
||||
phoneControllers.forEach((p) => p.dispose());
|
||||
for (final c in emailControllers) {
|
||||
c.dispose();
|
||||
}
|
||||
for (final c in phoneControllers) {
|
||||
c.dispose();
|
||||
}
|
||||
Get.delete<AddContactController>();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- COMMON WIDGETS ---
|
||||
InputDecoration _inputDecoration(String hint) => InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
||||
@ -142,19 +134,21 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
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,
|
||||
);
|
||||
|
||||
Widget _buildLabeledRow(
|
||||
String label,
|
||||
RxString selectedLabel,
|
||||
List<String> options,
|
||||
String inputLabel,
|
||||
TextEditingController controller,
|
||||
TextInputType inputType,
|
||||
{VoidCallback? onRemove}) {
|
||||
// DRY'd: LABELED FIELD ROW (used for phone/email)
|
||||
Widget _buildLabeledRow({
|
||||
required String label,
|
||||
required RxString selectedLabel,
|
||||
required List<String> options,
|
||||
required String inputLabel,
|
||||
required TextEditingController controller,
|
||||
required TextInputType inputType,
|
||||
VoidCallback? onRemove,
|
||||
Widget? suffixIcon,
|
||||
}) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -165,9 +159,10 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
MyText.labelMedium(label),
|
||||
MySpacing.height(8),
|
||||
_popupSelector(
|
||||
hint: "Label",
|
||||
selectedValue: selectedLabel,
|
||||
options: options),
|
||||
hint: "Label",
|
||||
selectedValue: selectedLabel,
|
||||
options: options,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -187,33 +182,17 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
: [],
|
||||
decoration: _inputDecoration("Enter $inputLabel").copyWith(
|
||||
counterText: "",
|
||||
suffixIcon: inputType == TextInputType.phone
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.contact_phone,
|
||||
color: Colors.blue),
|
||||
onPressed: () async {
|
||||
final selectedPhone =
|
||||
await ContactPickerHelper.pickIndianPhoneNumber(
|
||||
context);
|
||||
if (selectedPhone != null) {
|
||||
controller.text = selectedPhone;
|
||||
}
|
||||
},
|
||||
)
|
||||
: null,
|
||||
suffixIcon: suffixIcon,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty)
|
||||
return "$inputLabel is required";
|
||||
if (value == null || value.trim().isEmpty) return null;
|
||||
final trimmed = value.trim();
|
||||
if (inputType == TextInputType.phone) {
|
||||
if (!RegExp(r'^[1-9][0-9]{9}$').hasMatch(trimmed)) {
|
||||
return "Enter valid phone number";
|
||||
}
|
||||
if (inputType == TextInputType.phone &&
|
||||
!RegExp(r'^[1-9][0-9]{9}$').hasMatch(trimmed)) {
|
||||
return "Enter valid phone number";
|
||||
}
|
||||
if (inputType == TextInputType.emailAddress &&
|
||||
!RegExp(r'^[\w.-]+@([\w-]+\.)+[\w-]{2,4}$')
|
||||
.hasMatch(trimmed)) {
|
||||
!RegExp(r'^[\w.-]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(trimmed)) {
|
||||
return "Enter valid email";
|
||||
}
|
||||
return null;
|
||||
@ -234,94 +213,110 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmailList() => Column(
|
||||
children: List.generate(emailControllers.length, (index) {
|
||||
// DRY: List builder for email/phone fields
|
||||
Widget _buildDynamicList({
|
||||
required RxList<TextEditingController> ctrls,
|
||||
required RxList<RxString> labels,
|
||||
required List<String> labelOptions,
|
||||
required String label,
|
||||
required String inputLabel,
|
||||
required TextInputType inputType,
|
||||
required RxList listToRemoveFrom,
|
||||
Widget? phoneSuffixIcon,
|
||||
}) {
|
||||
return Obx(() {
|
||||
return Column(
|
||||
children: List.generate(ctrls.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
|
||||
label: label,
|
||||
selectedLabel: labels[index],
|
||||
options: labelOptions,
|
||||
inputLabel: inputLabel,
|
||||
controller: ctrls[index],
|
||||
inputType: inputType,
|
||||
onRemove: ctrls.length > 1
|
||||
? () {
|
||||
emailControllers.removeAt(index);
|
||||
emailLabels.removeAt(index);
|
||||
ctrls.removeAt(index);
|
||||
labels.removeAt(index);
|
||||
}
|
||||
: null,
|
||||
suffixIcon: phoneSuffixIcon != null && inputType == TextInputType.phone
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.contact_phone, color: Colors.blue),
|
||||
onPressed: () async {
|
||||
final selectedPhone =
|
||||
await ContactPickerHelper.pickIndianPhoneNumber(context);
|
||||
if (selectedPhone != null) {
|
||||
ctrls[index].text = selectedPhone;
|
||||
}
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildPhoneList() => 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 _buildEmailList() => _buildDynamicList(
|
||||
ctrls: emailControllers,
|
||||
labels: emailLabels,
|
||||
labelOptions: ["Office", "Personal", "Other"],
|
||||
label: "Email Label",
|
||||
inputLabel: "Email",
|
||||
inputType: TextInputType.emailAddress,
|
||||
listToRemoveFrom: emailControllers,
|
||||
);
|
||||
|
||||
Widget _buildPhoneList() => _buildDynamicList(
|
||||
ctrls: phoneControllers,
|
||||
labels: phoneLabels,
|
||||
labelOptions: ["Work", "Mobile", "Other"],
|
||||
label: "Phone Label",
|
||||
inputLabel: "Phone",
|
||||
inputType: TextInputType.phone,
|
||||
listToRemoveFrom: phoneControllers,
|
||||
phoneSuffixIcon: const Icon(Icons.contact_phone, color: Colors.blue),
|
||||
);
|
||||
|
||||
Widget _popupSelector({
|
||||
required String hint,
|
||||
required RxString selectedValue,
|
||||
required List<String> options,
|
||||
}) {
|
||||
return Obx(() => GestureDetector(
|
||||
onTap: () async {
|
||||
final selected = await showMenu<String>(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(100, 300, 100, 0),
|
||||
items: options.map((option) {
|
||||
return PopupMenuItem<String>(
|
||||
value: option,
|
||||
child: Text(option),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
|
||||
if (selected != null) {
|
||||
selectedValue.value = selected;
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
}) =>
|
||||
Obx(() => GestureDetector(
|
||||
onTap: () async {
|
||||
final selected = await showMenu<String>(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(100, 300, 100, 0),
|
||||
items: options.map((option) => PopupMenuItem<String>(value: option, child: Text(option))).toList(),
|
||||
);
|
||||
if (selected != null) selectedValue.value = selected;
|
||||
},
|
||||
child: Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
selectedValue.value.isNotEmpty ? selectedValue.value : hint,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
const Icon(Icons.expand_more, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
selectedValue.value.isNotEmpty ? selectedValue.value : hint,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
const Icon(Icons.expand_more, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
));
|
||||
|
||||
Widget _sectionLabel(String title) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -332,6 +327,7 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
],
|
||||
);
|
||||
|
||||
// CHIP list for tags
|
||||
Widget _tagInputSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -350,16 +346,14 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
),
|
||||
),
|
||||
Obx(() => controller.filteredSuggestions.isEmpty
|
||||
? const SizedBox()
|
||||
? const SizedBox.shrink()
|
||||
: Container(
|
||||
margin: const EdgeInsets.only(top: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black12, blurRadius: 4)
|
||||
],
|
||||
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 4)],
|
||||
),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
@ -392,145 +386,233 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextField(String label, TextEditingController controller,
|
||||
{int maxLines = 1}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.labelMedium(label),
|
||||
MySpacing.height(8),
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
maxLines: maxLines,
|
||||
decoration: _inputDecoration("Enter $label"),
|
||||
validator: (value) => value == null || value.trim().isEmpty
|
||||
? "$label is required"
|
||||
: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
// ---- REQUIRED FIELD (reusable)
|
||||
Widget _buildTextField(
|
||||
String label,
|
||||
TextEditingController controller, {
|
||||
int maxLines = 1,
|
||||
bool required = false,
|
||||
}) =>
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.labelMedium(label),
|
||||
MySpacing.height(8),
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
maxLines: maxLines,
|
||||
decoration: _inputDecoration("Enter $label"),
|
||||
validator: required
|
||||
? (value) =>
|
||||
value == null || value.trim().isEmpty ? "$label is required" : null
|
||||
: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildOrganizationField() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.labelMedium("Organization"),
|
||||
MySpacing.height(8),
|
||||
TextField(
|
||||
controller: orgController,
|
||||
onChanged: controller.filterOrganizationSuggestions,
|
||||
decoration: _inputDecoration("Enter organization"),
|
||||
),
|
||||
Obx(() => controller.filteredOrgSuggestions.isEmpty
|
||||
? const SizedBox()
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: controller.filteredOrgSuggestions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final suggestion = controller.filteredOrgSuggestions[index];
|
||||
return ListTile(
|
||||
dense: true,
|
||||
title: Text(suggestion),
|
||||
onTap: () {
|
||||
orgController.text = suggestion;
|
||||
controller.filteredOrgSuggestions.clear();
|
||||
},
|
||||
// -- Organization as required TextFormField
|
||||
Widget _buildOrganizationField() => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.labelMedium("Organization"),
|
||||
MySpacing.height(8),
|
||||
TextFormField(
|
||||
controller: orgController,
|
||||
onChanged: controller.filterOrganizationSuggestions,
|
||||
decoration: _inputDecoration("Enter organization"),
|
||||
validator: (value) =>
|
||||
value == null || value.trim().isEmpty ? "Organization is required" : null,
|
||||
),
|
||||
Obx(() => controller.filteredOrgSuggestions.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: controller.filteredOrgSuggestions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final suggestion = controller.filteredOrgSuggestions[index];
|
||||
return ListTile(
|
||||
dense: true,
|
||||
title: Text(suggestion),
|
||||
onTap: () {
|
||||
orgController.text = suggestion;
|
||||
controller.filteredOrgSuggestions.clear();
|
||||
},
|
||||
);
|
||||
},
|
||||
)),
|
||||
],
|
||||
);
|
||||
|
||||
// Action button row
|
||||
Widget _buildActionButtons() => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.delete<AddContactController>();
|
||||
},
|
||||
icon: const Icon(Icons.close, color: Colors.red),
|
||||
label: MyText.bodyMedium("Cancel", color: Colors.red, fontWeight: 600),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: const BorderSide(color: Colors.red),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
),
|
||||
),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// Validate bucket first in UI and show error under dropdown if empty
|
||||
bool valid = formKey.currentState!.validate();
|
||||
if (controller.selectedBucket.value.isEmpty) {
|
||||
bucketError.value = "Bucket is required";
|
||||
valid = false;
|
||||
} else {
|
||||
bucketError.value = "";
|
||||
}
|
||||
if (valid) {
|
||||
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(
|
||||
id: widget.existingContact?.id,
|
||||
name: nameController.text.trim(),
|
||||
organization: orgController.text.trim(),
|
||||
emails: emails,
|
||||
phones: phones,
|
||||
address: addressController.text.trim(),
|
||||
description: descriptionController.text.trim(),
|
||||
);
|
||||
},
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.delete<AddContactController>();
|
||||
},
|
||||
icon: const Icon(Icons.close, color: Colors.red),
|
||||
label:
|
||||
MyText.bodyMedium("Cancel", color: Colors.red, fontWeight: 600),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: const BorderSide(color: Colors.red),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
),
|
||||
),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
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(
|
||||
id: widget.existingContact?.id,
|
||||
name: nameController.text.trim(),
|
||||
organization: orgController.text.trim(),
|
||||
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),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.indigo,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.check_circle_outline, color: Colors.white),
|
||||
label: MyText.bodyMedium("Save", color: Colors.white, fontWeight: 600),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.indigo,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Projects multi-select section
|
||||
Widget _projectSelectorUI() {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return AlertDialog(
|
||||
title: const Text('Select Projects'),
|
||||
content: Obx(() => SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: controller.globalProjects.map((project) {
|
||||
final isSelected = controller.selectedProjects.contains(project);
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
checkboxTheme: CheckboxThemeData(
|
||||
fillColor: MaterialStateProperty.resolveWith<Color>(
|
||||
(states) => states.contains(MaterialState.selected)
|
||||
? Colors.white
|
||||
: Colors.transparent),
|
||||
checkColor: MaterialStateProperty.all(Colors.black),
|
||||
side: const BorderSide(color: Colors.black, width: 2),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: CheckboxListTile(
|
||||
dense: true,
|
||||
title: Text(project),
|
||||
value: isSelected,
|
||||
onChanged: (selected) {
|
||||
if (selected == true) {
|
||||
controller.selectedProjects.add(project);
|
||||
} else {
|
||||
controller.selectedProjects.remove(project);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Done'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Obx(() {
|
||||
final selected = controller.selectedProjects;
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
selected.isEmpty ? "Select Projects" : selected.join(', '),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
const Icon(Icons.expand_more, size: 20),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- MAIN BUILD ---
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
if (!controller.isInitialized.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(
|
||||
top: 32,
|
||||
).add(MediaQuery.of(context).viewInsets),
|
||||
padding: EdgeInsets.only(top: 32).add(MediaQuery.of(context).viewInsets),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
|
||||
@ -541,25 +623,44 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
children: [
|
||||
Center(
|
||||
child: MyText.titleMedium(
|
||||
widget.existingContact != null
|
||||
? "Edit Contact"
|
||||
: "Create New Contact",
|
||||
widget.existingContact != null ? "Edit Contact" : "Create New Contact",
|
||||
fontWeight: 700,
|
||||
),
|
||||
),
|
||||
MySpacing.height(24),
|
||||
_sectionLabel("Required Fields"),
|
||||
MySpacing.height(12),
|
||||
_buildTextField("Name", nameController),
|
||||
_buildTextField("Name", nameController, required: true),
|
||||
MySpacing.height(16),
|
||||
_buildOrganizationField(),
|
||||
MySpacing.height(16),
|
||||
MyText.labelMedium("Select Bucket"),
|
||||
MySpacing.height(8),
|
||||
_popupSelector(
|
||||
hint: "Select Bucket",
|
||||
selectedValue: controller.selectedBucket,
|
||||
options: controller.buckets,
|
||||
Stack(
|
||||
children: [
|
||||
_popupSelector(
|
||||
hint: "Select Bucket",
|
||||
selectedValue: controller.selectedBucket,
|
||||
options: controller.buckets,
|
||||
),
|
||||
// Validation message for bucket
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 56,
|
||||
child: Obx(
|
||||
() => bucketError.value.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
||||
child: Text(
|
||||
bucketError.value,
|
||||
style: const TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(24),
|
||||
Obx(() => GestureDetector(
|
||||
@ -567,11 +668,8 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.labelLarge("Advanced Details (Optional)",
|
||||
fontWeight: 600),
|
||||
Icon(showAdvanced.value
|
||||
? Icons.expand_less
|
||||
: Icons.expand_more),
|
||||
MyText.labelLarge("Advanced Details (Optional)", fontWeight: 600),
|
||||
Icon(showAdvanced.value ? Icons.expand_less : Icons.expand_more),
|
||||
],
|
||||
),
|
||||
)),
|
||||
@ -619,15 +717,12 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
MySpacing.height(8),
|
||||
_tagInputSection(),
|
||||
MySpacing.height(16),
|
||||
_buildTextField("Address", addressController,
|
||||
maxLines: 2),
|
||||
_buildTextField("Address", addressController, maxLines: 2, required: false),
|
||||
MySpacing.height(16),
|
||||
_buildTextField(
|
||||
"Description", descriptionController,
|
||||
maxLines: 2),
|
||||
_buildTextField("Description", descriptionController, maxLines: 2, required: false),
|
||||
],
|
||||
)
|
||||
: const SizedBox()),
|
||||
: const SizedBox.shrink()),
|
||||
MySpacing.height(24),
|
||||
_buildActionButtons(),
|
||||
],
|
||||
@ -639,95 +734,4 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _projectSelectorUI() {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Select Projects'),
|
||||
content: Obx(() {
|
||||
return SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: controller.globalProjects.map((project) {
|
||||
final isSelected =
|
||||
controller.selectedProjects.contains(project);
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
unselectedWidgetColor: Colors.black,
|
||||
checkboxTheme: CheckboxThemeData(
|
||||
fillColor: MaterialStateProperty.resolveWith<Color>(
|
||||
(states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return Colors.white;
|
||||
}
|
||||
return Colors.transparent;
|
||||
}),
|
||||
checkColor: MaterialStateProperty.all(Colors.black),
|
||||
side:
|
||||
const BorderSide(color: Colors.black, width: 2),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: CheckboxListTile(
|
||||
dense: true,
|
||||
title: Text(project),
|
||||
value: isSelected,
|
||||
onChanged: (bool? selected) {
|
||||
if (selected == true) {
|
||||
controller.selectedProjects.add(project);
|
||||
} else {
|
||||
controller.selectedProjects.remove(project);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Done'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Obx(() {
|
||||
final selected = controller.selectedProjects;
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
selected.isEmpty ? "Select Projects" : selected.join(', '),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
const Icon(Icons.expand_more, size: 20),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -16,32 +16,20 @@ import 'package:marco/model/directory/add_comment_bottom_sheet.dart';
|
||||
import 'package:marco/helpers/utils/date_time_utils.dart';
|
||||
import 'package:marco/model/directory/add_contact_bottom_sheet.dart';
|
||||
|
||||
class ContactDetailScreen extends StatefulWidget {
|
||||
final ContactModel contact;
|
||||
|
||||
const ContactDetailScreen({super.key, required this.contact});
|
||||
|
||||
@override
|
||||
State<ContactDetailScreen> createState() => _ContactDetailScreenState();
|
||||
}
|
||||
|
||||
// HELPER: Delta to HTML conversion
|
||||
String _convertDeltaToHtml(dynamic delta) {
|
||||
final buffer = StringBuffer();
|
||||
bool inList = false;
|
||||
|
||||
for (var op in delta.toList()) {
|
||||
final data = op.data?.toString() ?? '';
|
||||
final String data = op.data?.toString() ?? '';
|
||||
final attr = op.attributes ?? {};
|
||||
final bool isListItem = attr.containsKey('list');
|
||||
|
||||
final isListItem = attr.containsKey('list');
|
||||
|
||||
// Start list
|
||||
if (isListItem && !inList) {
|
||||
buffer.write('<ul>');
|
||||
inList = true;
|
||||
}
|
||||
|
||||
// Close list if we are not in list mode anymore
|
||||
if (!isListItem && inList) {
|
||||
buffer.write('</ul>');
|
||||
inList = false;
|
||||
@ -49,15 +37,12 @@ String _convertDeltaToHtml(dynamic delta) {
|
||||
|
||||
if (isListItem) buffer.write('<li>');
|
||||
|
||||
// Apply inline styles
|
||||
if (attr.containsKey('bold')) buffer.write('<strong>');
|
||||
if (attr.containsKey('italic')) buffer.write('<em>');
|
||||
if (attr.containsKey('underline')) buffer.write('<u>');
|
||||
if (attr.containsKey('strike')) buffer.write('<s>');
|
||||
if (attr.containsKey('link')) buffer.write('<a href="${attr['link']}">');
|
||||
|
||||
buffer.write(data.replaceAll('\n', ''));
|
||||
|
||||
if (attr.containsKey('link')) buffer.write('</a>');
|
||||
if (attr.containsKey('strike')) buffer.write('</s>');
|
||||
if (attr.containsKey('underline')) buffer.write('</u>');
|
||||
@ -66,14 +51,21 @@ String _convertDeltaToHtml(dynamic delta) {
|
||||
|
||||
if (isListItem)
|
||||
buffer.write('</li>');
|
||||
else if (data.contains('\n')) buffer.write('<br>');
|
||||
else if (data.contains('\n')) {
|
||||
buffer.write('<br>');
|
||||
}
|
||||
}
|
||||
|
||||
if (inList) buffer.write('</ul>');
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
class ContactDetailScreen extends StatefulWidget {
|
||||
final ContactModel contact;
|
||||
const ContactDetailScreen({super.key, required this.contact});
|
||||
@override
|
||||
State<ContactDetailScreen> createState() => _ContactDetailScreenState();
|
||||
}
|
||||
|
||||
class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
late final DirectoryController directoryController;
|
||||
late final ProjectController projectController;
|
||||
@ -85,7 +77,6 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
directoryController = Get.find<DirectoryController>();
|
||||
projectController = Get.find<ProjectController>();
|
||||
contact = widget.contact;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
directoryController.fetchCommentsForContact(contact.id);
|
||||
});
|
||||
@ -103,13 +94,12 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSubHeader(),
|
||||
const Divider(height: 1, thickness: 0.5, color: Colors.grey),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
_buildDetailsTab(),
|
||||
_buildCommentsTab(context),
|
||||
],
|
||||
),
|
||||
child: TabBarView(children: [
|
||||
_buildDetailsTab(),
|
||||
_buildCommentsTab(context),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -130,10 +120,8 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new,
|
||||
color: Colors.black, size: 20),
|
||||
onPressed: () =>
|
||||
Get.offAllNamed('/dashboard/directory-main-page'),
|
||||
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black, size: 20),
|
||||
onPressed: () => Get.offAllNamed('/dashboard/directory-main-page'),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
Expanded(
|
||||
@ -141,30 +129,10 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MyText.titleLarge('Contact Profile',
|
||||
fontWeight: 700, color: Colors.black),
|
||||
MyText.titleLarge('Contact Profile', fontWeight: 700, color: Colors.black),
|
||||
MySpacing.height(2),
|
||||
GetBuilder<ProjectController>(
|
||||
builder: (projectController) {
|
||||
final projectName =
|
||||
projectController.selectedProject?.name ??
|
||||
'Select Project';
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Icons.work_outline,
|
||||
size: 14, color: Colors.grey),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.bodySmall(
|
||||
projectName,
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
builder: (p) => ProjectLabel(p.selectedProject?.name),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -176,38 +144,30 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
}
|
||||
|
||||
Widget _buildSubHeader() {
|
||||
final firstName = contact.name.split(" ").first;
|
||||
final lastName = contact.name.split(" ").length > 1 ? contact.name.split(" ").last : "";
|
||||
|
||||
return Padding(
|
||||
padding: MySpacing.xy(16, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Avatar(
|
||||
firstName: contact.name.split(" ").first,
|
||||
lastName: contact.name.split(" ").length > 1
|
||||
? contact.name.split(" ").last
|
||||
: "",
|
||||
size: 35,
|
||||
backgroundColor: Colors.indigo,
|
||||
),
|
||||
MySpacing.width(12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleSmall(contact.name,
|
||||
fontWeight: 600, color: Colors.black),
|
||||
MySpacing.height(2),
|
||||
MyText.bodySmall(contact.organization,
|
||||
fontWeight: 500, color: Colors.grey[700]),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(children: [
|
||||
Avatar(firstName: firstName, lastName: lastName, size: 35, backgroundColor: Colors.indigo),
|
||||
MySpacing.width(12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleSmall(contact.name, fontWeight: 600, color: Colors.black),
|
||||
MySpacing.height(2),
|
||||
MyText.bodySmall(contact.organization, fontWeight: 500, color: Colors.grey[700]),
|
||||
],
|
||||
),
|
||||
]),
|
||||
TabBar(
|
||||
labelColor: Colors.red,
|
||||
unselectedLabelColor: Colors.black,
|
||||
indicator: MaterialIndicator(
|
||||
indicator: MaterialIndicator(
|
||||
color: Colors.red,
|
||||
height: 4,
|
||||
topLeftRadius: 8,
|
||||
@ -226,33 +186,37 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
}
|
||||
|
||||
Widget _buildDetailsTab() {
|
||||
final email = contact.contactEmails.isNotEmpty
|
||||
? contact.contactEmails.first.emailAddress
|
||||
: "-";
|
||||
|
||||
final phone = contact.contactPhones.isNotEmpty
|
||||
? contact.contactPhones.first.phoneNumber
|
||||
: "-";
|
||||
|
||||
final tags = contact.tags.map((e) => e.name).join(", ");
|
||||
|
||||
final bucketNames = contact.bucketIds
|
||||
.map((id) => directoryController.contactBuckets
|
||||
.firstWhereOrNull((b) => b.id == id)
|
||||
?.name)
|
||||
.whereType<String>()
|
||||
.join(", ");
|
||||
|
||||
final projectNames = contact.projectIds
|
||||
?.map((id) => projectController.projects
|
||||
.firstWhereOrNull((p) => p.id == id)
|
||||
?.name)
|
||||
.whereType<String>()
|
||||
.join(", ") ??
|
||||
"-";
|
||||
|
||||
final projectNames = contact.projectIds?.map((id) =>
|
||||
projectController.projects.firstWhereOrNull((p) => p.id == id)?.name).whereType<String>().join(", ") ?? "-";
|
||||
final category = contact.contactCategory?.name ?? "-";
|
||||
|
||||
Widget multiRows({required List<dynamic> items, required IconData icon, required String label, required String typeLabel, required Function(String)? onTap, required Function(String)? onLongPress}) {
|
||||
return items.isNotEmpty
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_iconInfoRow(icon, label, items.first, onTap: () => onTap?.call(items.first), onLongPress: () => onLongPress?.call(items.first)),
|
||||
...items.skip(1).map(
|
||||
(val) => _iconInfoRow(
|
||||
null,
|
||||
'',
|
||||
val,
|
||||
onTap: () => onTap?.call(val),
|
||||
onLongPress: () => onLongPress?.call(val),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: _iconInfoRow(icon, label, "-");
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
@ -261,28 +225,38 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(12),
|
||||
// BASIC INFO CARD
|
||||
_infoCard("Basic Info", [
|
||||
_iconInfoRow(Icons.email, "Email", email,
|
||||
onTap: () => LauncherUtils.launchEmail(email),
|
||||
onLongPress: () => LauncherUtils.copyToClipboard(email,
|
||||
typeLabel: "Email")),
|
||||
_iconInfoRow(Icons.phone, "Phone", phone,
|
||||
onTap: () => LauncherUtils.launchPhone(phone),
|
||||
onLongPress: () => LauncherUtils.copyToClipboard(phone,
|
||||
typeLabel: "Phone")),
|
||||
multiRows(
|
||||
items: contact.contactEmails.map((e) => e.emailAddress).toList(),
|
||||
icon: Icons.email,
|
||||
label: "Email",
|
||||
typeLabel: "Email",
|
||||
onTap: (email) => LauncherUtils.launchEmail(email),
|
||||
onLongPress: (email) => LauncherUtils.copyToClipboard(email, typeLabel: "Email"),
|
||||
),
|
||||
multiRows(
|
||||
items: contact.contactPhones.map((p) => p.phoneNumber).toList(),
|
||||
icon: Icons.phone,
|
||||
label: "Phone",
|
||||
typeLabel: "Phone",
|
||||
onTap: (phone) => LauncherUtils.launchPhone(phone),
|
||||
onLongPress: (phone) => LauncherUtils.copyToClipboard(phone, typeLabel: "Phone"),
|
||||
),
|
||||
_iconInfoRow(Icons.location_on, "Address", contact.address),
|
||||
]),
|
||||
// ORGANIZATION CARD
|
||||
_infoCard("Organization", [
|
||||
_iconInfoRow(
|
||||
Icons.business, "Organization", contact.organization),
|
||||
_iconInfoRow(Icons.business, "Organization", contact.organization),
|
||||
_iconInfoRow(Icons.category, "Category", category),
|
||||
]),
|
||||
// META INFO CARD
|
||||
_infoCard("Meta Info", [
|
||||
_iconInfoRow(Icons.label, "Tags", tags.isNotEmpty ? tags : "-"),
|
||||
_iconInfoRow(Icons.folder_shared, "Contact Buckets",
|
||||
bucketNames.isNotEmpty ? bucketNames : "-"),
|
||||
_iconInfoRow(Icons.folder_shared, "Contact Buckets", bucketNames.isNotEmpty ? bucketNames : "-"),
|
||||
_iconInfoRow(Icons.work_outline, "Projects", projectNames),
|
||||
]),
|
||||
// DESCRIPTION CARD
|
||||
_infoCard("Description", [
|
||||
MySpacing.height(6),
|
||||
Align(
|
||||
@ -294,7 +268,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
])
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -309,25 +283,17 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
await directoryController.fetchContacts();
|
||||
final updated =
|
||||
directoryController.allContacts.firstWhereOrNull(
|
||||
(c) => c.id == contact.id,
|
||||
);
|
||||
directoryController.allContacts.firstWhereOrNull((c) => c.id == contact.id);
|
||||
if (updated != null) {
|
||||
setState(() {
|
||||
contact = updated;
|
||||
});
|
||||
setState(() => contact = updated);
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.edit, color: Colors.white),
|
||||
label: const Text(
|
||||
"Edit Contact",
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
label: const Text("Edit Contact", style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -337,24 +303,17 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
Widget _buildCommentsTab(BuildContext context) {
|
||||
return Obx(() {
|
||||
final contactId = contact.id;
|
||||
|
||||
if (!directoryController.contactCommentsMap.containsKey(contactId)) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final comments = directoryController
|
||||
.getCommentsForContact(contactId)
|
||||
.reversed
|
||||
.toList();
|
||||
|
||||
final comments = directoryController.getCommentsForContact(contactId).reversed.toList();
|
||||
final editingId = directoryController.editingCommentId.value;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
comments.isEmpty
|
||||
? Center(
|
||||
child:
|
||||
MyText.bodyLarge("No comments yet.", color: Colors.grey),
|
||||
? Center(
|
||||
child: MyText.bodyLarge("No comments yet.", color: Colors.grey),
|
||||
)
|
||||
: Padding(
|
||||
padding: MySpacing.xy(12, 12),
|
||||
@ -362,137 +321,10 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
padding: const EdgeInsets.only(bottom: 100),
|
||||
itemCount: comments.length,
|
||||
separatorBuilder: (_, __) => MySpacing.height(14),
|
||||
itemBuilder: (_, index) {
|
||||
final comment = comments[index];
|
||||
final isEditing = editingId == comment.id;
|
||||
|
||||
final initials = comment.createdBy.firstName.isNotEmpty
|
||||
? comment.createdBy.firstName[0].toUpperCase()
|
||||
: "?";
|
||||
|
||||
final decodedDelta = HtmlToDelta().convert(comment.note);
|
||||
|
||||
final quillController = isEditing
|
||||
? quill.QuillController(
|
||||
document: quill.Document.fromDelta(decodedDelta),
|
||||
selection: TextSelection.collapsed(
|
||||
offset: decodedDelta.length),
|
||||
)
|
||||
: null;
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
padding: MySpacing.xy(8, 7),
|
||||
decoration: BoxDecoration(
|
||||
color: isEditing ? Colors.indigo[50] : Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isEditing
|
||||
? Colors.indigo
|
||||
: Colors.grey.shade300,
|
||||
width: 1.2,
|
||||
),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header Row
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Avatar(
|
||||
firstName: initials,
|
||||
lastName: '',
|
||||
size: 36),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium(
|
||||
"By: ${comment.createdBy.firstName}",
|
||||
fontWeight: 600,
|
||||
color: Colors.indigo[800],
|
||||
),
|
||||
MySpacing.height(4),
|
||||
MyText.bodySmall(
|
||||
DateTimeUtils.convertUtcToLocal(
|
||||
comment.createdAt.toString(),
|
||||
format: 'dd MMM yyyy, hh:mm a',
|
||||
),
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isEditing ? Icons.close : Icons.edit,
|
||||
size: 20,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
onPressed: () {
|
||||
directoryController.editingCommentId.value =
|
||||
isEditing ? null : comment.id;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
// Comment Content
|
||||
if (isEditing && quillController != null)
|
||||
CommentEditorCard(
|
||||
controller: quillController,
|
||||
onCancel: () {
|
||||
directoryController.editingCommentId.value =
|
||||
null;
|
||||
},
|
||||
onSave: (controller) async {
|
||||
final delta = controller.document.toDelta();
|
||||
final htmlOutput = _convertDeltaToHtml(delta);
|
||||
final updated =
|
||||
comment.copyWith(note: htmlOutput);
|
||||
|
||||
await directoryController
|
||||
.updateComment(updated);
|
||||
|
||||
// ✅ Re-fetch comments to get updated list
|
||||
await directoryController
|
||||
.fetchCommentsForContact(contactId);
|
||||
|
||||
// ✅ Exit editing mode
|
||||
directoryController.editingCommentId.value =
|
||||
null;
|
||||
},
|
||||
)
|
||||
else
|
||||
html.Html(
|
||||
data: comment.note,
|
||||
style: {
|
||||
"body": html.Style(
|
||||
margin: html.Margins.zero,
|
||||
padding: html.HtmlPaddings.zero,
|
||||
fontSize: html.FontSize.medium,
|
||||
color: Colors.black87,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
itemBuilder: (_, index) => _buildCommentItem(comments[index], editingId, contact.id),
|
||||
),
|
||||
),
|
||||
|
||||
// Floating Action Button
|
||||
if (directoryController.editingCommentId.value == null)
|
||||
if (editingId == null)
|
||||
Positioned(
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
@ -503,17 +335,12 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
AddCommentBottomSheet(contactId: contactId),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
await directoryController
|
||||
.fetchCommentsForContact(contactId);
|
||||
await directoryController.fetchCommentsForContact(contactId);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.add_comment, color: Colors.white),
|
||||
label: const Text(
|
||||
"Add Comment",
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
label: const Text("Add Comment", style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -521,25 +348,127 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
Widget _iconInfoRow(IconData icon, String label, String value,
|
||||
{VoidCallback? onTap, VoidCallback? onLongPress}) {
|
||||
Widget _buildCommentItem(comment, editingId, contactId) {
|
||||
final isEditing = editingId == comment.id;
|
||||
final initials = comment.createdBy.firstName.isNotEmpty
|
||||
? comment.createdBy.firstName[0].toUpperCase()
|
||||
: "?";
|
||||
final decodedDelta = HtmlToDelta().convert(comment.note);
|
||||
final quillController = isEditing
|
||||
? quill.QuillController(
|
||||
document: quill.Document.fromDelta(decodedDelta),
|
||||
selection: TextSelection.collapsed(offset: decodedDelta.length),
|
||||
)
|
||||
: null;
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
padding: MySpacing.xy(8, 7),
|
||||
decoration: BoxDecoration(
|
||||
color: isEditing ? Colors.indigo[50] : Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isEditing ? Colors.indigo : Colors.grey.shade300,
|
||||
width: 1.2,
|
||||
),
|
||||
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 4, offset: Offset(0, 2))],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header Row
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Avatar(firstName: initials, lastName: '', size: 36),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium("By: ${comment.createdBy.firstName}",
|
||||
fontWeight: 600, color: Colors.indigo[800]),
|
||||
MySpacing.height(4),
|
||||
MyText.bodySmall(
|
||||
DateTimeUtils.convertUtcToLocal(
|
||||
comment.createdAt.toString(),
|
||||
format: 'dd MMM yyyy, hh:mm a',
|
||||
),
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isEditing ? Icons.close : Icons.edit,
|
||||
size: 20,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
onPressed: () {
|
||||
directoryController.editingCommentId.value = isEditing ? null : comment.id;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
// Comment Content
|
||||
if (isEditing && quillController != null)
|
||||
CommentEditorCard(
|
||||
controller: quillController,
|
||||
onCancel: () => directoryController.editingCommentId.value = null,
|
||||
onSave: (ctrl) async {
|
||||
final delta = ctrl.document.toDelta();
|
||||
final htmlOutput = _convertDeltaToHtml(delta);
|
||||
final updated = comment.copyWith(note: htmlOutput);
|
||||
await directoryController.updateComment(updated);
|
||||
await directoryController.fetchCommentsForContact(contactId);
|
||||
directoryController.editingCommentId.value = null;
|
||||
},
|
||||
)
|
||||
else
|
||||
html.Html(
|
||||
data: comment.note,
|
||||
style: {
|
||||
"body": html.Style(
|
||||
margin: html.Margins.zero,
|
||||
padding: html.HtmlPaddings.zero,
|
||||
fontSize: html.FontSize.medium,
|
||||
color: Colors.black87,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _iconInfoRow(
|
||||
IconData? icon,
|
||||
String label,
|
||||
String value, {
|
||||
VoidCallback? onTap,
|
||||
VoidCallback? onLongPress,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: MySpacing.y(8),
|
||||
padding: MySpacing.y(2),
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, size: 22, color: Colors.indigo),
|
||||
MySpacing.width(12),
|
||||
if (icon != null) ...[
|
||||
Icon(icon, size: 22, color: Colors.indigo),
|
||||
MySpacing.width(12),
|
||||
] else
|
||||
const SizedBox(width: 34),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodySmall(label,
|
||||
fontWeight: 600, color: Colors.black87),
|
||||
MySpacing.height(2),
|
||||
if (label.isNotEmpty)
|
||||
MyText.bodySmall(label, fontWeight: 600, color: Colors.black87),
|
||||
if (label.isNotEmpty) MySpacing.height(2),
|
||||
MyText.bodyMedium(value, color: Colors.grey[800]),
|
||||
],
|
||||
),
|
||||
@ -560,8 +489,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleSmall(title,
|
||||
fontWeight: 700, color: Colors.indigo[700]),
|
||||
MyText.titleSmall(title, fontWeight: 700, color: Colors.indigo[700]),
|
||||
MySpacing.height(8),
|
||||
...children,
|
||||
],
|
||||
@ -570,3 +498,26 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper widget for Project label in AppBar
|
||||
class ProjectLabel extends StatelessWidget {
|
||||
final String? projectName;
|
||||
const ProjectLabel(this.projectName, {super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Icons.work_outline, size: 14, color: Colors.grey),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.bodySmall(
|
||||
projectName ?? 'Select Project',
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -24,8 +24,7 @@ class DirectoryView extends StatefulWidget {
|
||||
class _DirectoryViewState extends State<DirectoryView> {
|
||||
final DirectoryController controller = Get.find();
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
final PermissionController permissionController =
|
||||
Get.put(PermissionController());
|
||||
final PermissionController permissionController = Get.put(PermissionController());
|
||||
|
||||
Future<void> _refreshDirectory() async {
|
||||
try {
|
||||
@ -304,7 +303,6 @@ class _DirectoryViewState extends State<DirectoryView> {
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => const CreateBucketBottomSheet(),
|
||||
);
|
||||
|
||||
if (created == true) {
|
||||
await controller.fetchBuckets();
|
||||
}
|
||||
@ -442,62 +440,69 @@ class _DirectoryViewState extends State<DirectoryView> {
|
||||
color: Colors.grey[700],
|
||||
overflow: TextOverflow.ellipsis),
|
||||
MySpacing.height(8),
|
||||
...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(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
...contact.contactPhones.map((p) => Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 8, top: 4),
|
||||
|
||||
// Show only the first email (if present)
|
||||
if (contact.contactEmails.isNotEmpty)
|
||||
GestureDetector(
|
||||
onTap: () => LauncherUtils.launchEmail(
|
||||
contact.contactEmails.first.emailAddress),
|
||||
onLongPress: () =>
|
||||
LauncherUtils.copyToClipboard(
|
||||
contact.contactEmails.first.emailAddress,
|
||||
typeLabel: 'Email',
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () =>
|
||||
LauncherUtils.launchPhone(
|
||||
p.phoneNumber),
|
||||
const Icon(Icons.email_outlined,
|
||||
size: 16, color: Colors.indigo),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.labelSmall(
|
||||
contact.contactEmails.first.emailAddress,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.indigo,
|
||||
decoration:
|
||||
TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Show only the first phone (if present)
|
||||
if (contact.contactPhones.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 8, top: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => LauncherUtils
|
||||
.launchPhone(contact
|
||||
.contactPhones
|
||||
.first
|
||||
.phoneNumber),
|
||||
onLongPress: () =>
|
||||
LauncherUtils.copyToClipboard(
|
||||
p.phoneNumber,
|
||||
typeLabel: 'Phone'),
|
||||
contact.contactPhones.first
|
||||
.phoneNumber,
|
||||
typeLabel: 'Phone',
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.phone_outlined,
|
||||
const Icon(
|
||||
Icons.phone_outlined,
|
||||
size: 16,
|
||||
color: Colors.indigo),
|
||||
MySpacing.width(4),
|
||||
ConstrainedBox(
|
||||
constraints:
|
||||
const BoxConstraints(
|
||||
maxWidth: 140),
|
||||
Expanded(
|
||||
child: MyText.labelSmall(
|
||||
p.phoneNumber,
|
||||
contact.contactPhones.first
|
||||
.phoneNumber,
|
||||
overflow:
|
||||
TextOverflow.ellipsis,
|
||||
color: Colors.indigo,
|
||||
@ -508,19 +513,22 @@ class _DirectoryViewState extends State<DirectoryView> {
|
||||
],
|
||||
),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
GestureDetector(
|
||||
onTap: () =>
|
||||
LauncherUtils.launchWhatsApp(
|
||||
p.phoneNumber),
|
||||
child: const FaIcon(
|
||||
FontAwesomeIcons.whatsapp,
|
||||
color: Colors.green,
|
||||
size: 16),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
GestureDetector(
|
||||
onTap: () =>
|
||||
LauncherUtils.launchWhatsApp(
|
||||
contact.contactPhones.first
|
||||
.phoneNumber),
|
||||
child: const FaIcon(
|
||||
FontAwesomeIcons.whatsapp,
|
||||
color: Colors.green,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (tags.isNotEmpty) ...[
|
||||
MySpacing.height(2),
|
||||
MyText.labelSmall(tags.join(', '),
|
||||
|
Loading…
x
Reference in New Issue
Block a user