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> {
|
class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||||
final controller = Get.put(AddContactController());
|
// Controllers and state
|
||||||
|
final AddContactController controller = Get.put(AddContactController());
|
||||||
final formKey = GlobalKey<FormState>();
|
final formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
final nameController = TextEditingController();
|
final nameController = TextEditingController();
|
||||||
final orgController = TextEditingController();
|
final orgController = TextEditingController();
|
||||||
final addressController = TextEditingController();
|
final addressController = TextEditingController();
|
||||||
final descriptionController = TextEditingController();
|
final descriptionController = TextEditingController();
|
||||||
final tagTextController = 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 =
|
// Use Rx for advanced toggle and dynamic fields
|
||||||
<TextEditingController>[].obs;
|
final showAdvanced = false.obs;
|
||||||
final RxList<RxString> phoneLabels = <RxString>[].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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
controller.resetForm();
|
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();
|
void _initFields() {
|
||||||
phoneLabels.clear();
|
final c = widget.existingContact;
|
||||||
for (var phone in widget.existingContact!.contactPhones) {
|
if (c != null) {
|
||||||
phoneControllers.add(TextEditingController(text: phone.phoneNumber));
|
nameController.text = c.name;
|
||||||
phoneLabels.add((phone.label).obs);
|
orgController.text = c.organization;
|
||||||
|
addressController.text = c.address;
|
||||||
|
descriptionController.text = c.description ;
|
||||||
}
|
}
|
||||||
if (phoneControllers.isEmpty) {
|
if (c != null) {
|
||||||
phoneControllers.add(TextEditingController());
|
emailControllers.assignAll(c.contactEmails.isEmpty
|
||||||
phoneLabels.add('Work'.obs);
|
? [TextEditingController()]
|
||||||
}
|
: c.contactEmails.map((e) => TextEditingController(text: e.emailAddress)));
|
||||||
|
emailLabels.assignAll(c.contactEmails.isEmpty
|
||||||
controller.enteredTags.assignAll(
|
? ['Office'.obs]
|
||||||
widget.existingContact!.tags.map((tag) => tag.name).toList(),
|
: 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) {
|
ever(controller.isInitialized, (bool ready) {
|
||||||
if (ready) {
|
if (ready) {
|
||||||
final projectIds = widget.existingContact!.projectIds;
|
final projectIds = c.projectIds;
|
||||||
final bucketId = widget.existingContact!.bucketIds.firstOrNull;
|
final bucketId = c.bucketIds.firstOrNull;
|
||||||
final categoryName = widget.existingContact!.contactCategory?.name;
|
final categoryName = c.contactCategory?.name;
|
||||||
|
if (categoryName != null) controller.selectedCategory.value = categoryName;
|
||||||
if (categoryName != null) {
|
|
||||||
controller.selectedCategory.value = categoryName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (projectIds != null) {
|
if (projectIds != null) {
|
||||||
final names = projectIds
|
controller.selectedProjects.assignAll(
|
||||||
.map((id) {
|
projectIds //
|
||||||
return controller.projectsMap.entries
|
.map((id) => controller.projectsMap.entries
|
||||||
.firstWhereOrNull((e) => e.value == id)
|
.firstWhereOrNull((e) => e.value == id)
|
||||||
?.key;
|
?.key)
|
||||||
})
|
|
||||||
.whereType<String>()
|
.whereType<String>()
|
||||||
.toList();
|
.toList(),
|
||||||
controller.selectedProjects.assignAll(names);
|
);
|
||||||
}
|
}
|
||||||
if (bucketId != null) {
|
if (bucketId != null) {
|
||||||
final name = controller.bucketsMap.entries
|
final name = controller.bucketsMap.entries
|
||||||
.firstWhereOrNull((e) => e.value == bucketId)
|
.firstWhereOrNull((e) => e.value == bucketId)
|
||||||
?.key;
|
?.key;
|
||||||
if (name != null) {
|
if (name != null) controller.selectedBucket.value = name;
|
||||||
controller.selectedBucket.value = name;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -110,6 +96,7 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
phoneControllers.add(TextEditingController());
|
phoneControllers.add(TextEditingController());
|
||||||
phoneLabels.add('Work'.obs);
|
phoneLabels.add('Work'.obs);
|
||||||
}
|
}
|
||||||
|
tagTextController.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -119,12 +106,17 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
tagTextController.dispose();
|
tagTextController.dispose();
|
||||||
addressController.dispose();
|
addressController.dispose();
|
||||||
descriptionController.dispose();
|
descriptionController.dispose();
|
||||||
emailControllers.forEach((e) => e.dispose());
|
for (final c in emailControllers) {
|
||||||
phoneControllers.forEach((p) => p.dispose());
|
c.dispose();
|
||||||
|
}
|
||||||
|
for (final c in phoneControllers) {
|
||||||
|
c.dispose();
|
||||||
|
}
|
||||||
Get.delete<AddContactController>();
|
Get.delete<AddContactController>();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- COMMON WIDGETS ---
|
||||||
InputDecoration _inputDecoration(String hint) => InputDecoration(
|
InputDecoration _inputDecoration(String hint) => InputDecoration(
|
||||||
hintText: hint,
|
hintText: hint,
|
||||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
||||||
@ -142,19 +134,21 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: const BorderSide(color: Colors.blueAccent, width: 1.5),
|
borderSide: const BorderSide(color: Colors.blueAccent, width: 1.5),
|
||||||
),
|
),
|
||||||
contentPadding:
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
|
||||||
isDense: true,
|
isDense: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildLabeledRow(
|
// DRY'd: LABELED FIELD ROW (used for phone/email)
|
||||||
String label,
|
Widget _buildLabeledRow({
|
||||||
RxString selectedLabel,
|
required String label,
|
||||||
List<String> options,
|
required RxString selectedLabel,
|
||||||
String inputLabel,
|
required List<String> options,
|
||||||
TextEditingController controller,
|
required String inputLabel,
|
||||||
TextInputType inputType,
|
required TextEditingController controller,
|
||||||
{VoidCallback? onRemove}) {
|
required TextInputType inputType,
|
||||||
|
VoidCallback? onRemove,
|
||||||
|
Widget? suffixIcon,
|
||||||
|
}) {
|
||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -167,7 +161,8 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
_popupSelector(
|
_popupSelector(
|
||||||
hint: "Label",
|
hint: "Label",
|
||||||
selectedValue: selectedLabel,
|
selectedValue: selectedLabel,
|
||||||
options: options),
|
options: options,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -187,33 +182,17 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
: [],
|
: [],
|
||||||
decoration: _inputDecoration("Enter $inputLabel").copyWith(
|
decoration: _inputDecoration("Enter $inputLabel").copyWith(
|
||||||
counterText: "",
|
counterText: "",
|
||||||
suffixIcon: inputType == TextInputType.phone
|
suffixIcon: suffixIcon,
|
||||||
? 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,
|
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.trim().isEmpty)
|
if (value == null || value.trim().isEmpty) return null;
|
||||||
return "$inputLabel is required";
|
|
||||||
final trimmed = value.trim();
|
final trimmed = value.trim();
|
||||||
if (inputType == TextInputType.phone) {
|
if (inputType == TextInputType.phone &&
|
||||||
if (!RegExp(r'^[1-9][0-9]{9}$').hasMatch(trimmed)) {
|
!RegExp(r'^[1-9][0-9]{9}$').hasMatch(trimmed)) {
|
||||||
return "Enter valid phone number";
|
return "Enter valid phone number";
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (inputType == TextInputType.emailAddress &&
|
if (inputType == TextInputType.emailAddress &&
|
||||||
!RegExp(r'^[\w.-]+@([\w-]+\.)+[\w-]{2,4}$')
|
!RegExp(r'^[\w.-]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(trimmed)) {
|
||||||
.hasMatch(trimmed)) {
|
|
||||||
return "Enter valid email";
|
return "Enter valid email";
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -234,71 +213,88 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmailList() => Column(
|
// DRY: List builder for email/phone fields
|
||||||
children: List.generate(emailControllers.length, (index) {
|
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(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: _buildLabeledRow(
|
child: _buildLabeledRow(
|
||||||
"Email Label",
|
label: label,
|
||||||
emailLabels[index],
|
selectedLabel: labels[index],
|
||||||
["Office", "Personal", "Other"],
|
options: labelOptions,
|
||||||
"Email",
|
inputLabel: inputLabel,
|
||||||
emailControllers[index],
|
controller: ctrls[index],
|
||||||
TextInputType.emailAddress,
|
inputType: inputType,
|
||||||
onRemove: emailControllers.length > 1
|
onRemove: ctrls.length > 1
|
||||||
? () {
|
? () {
|
||||||
emailControllers.removeAt(index);
|
ctrls.removeAt(index);
|
||||||
emailLabels.removeAt(index);
|
labels.removeAt(index);
|
||||||
}
|
}
|
||||||
: null,
|
: 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 _buildEmailList() => _buildDynamicList(
|
||||||
|
ctrls: emailControllers,
|
||||||
|
labels: emailLabels,
|
||||||
|
labelOptions: ["Office", "Personal", "Other"],
|
||||||
|
label: "Email Label",
|
||||||
|
inputLabel: "Email",
|
||||||
|
inputType: TextInputType.emailAddress,
|
||||||
|
listToRemoveFrom: emailControllers,
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildPhoneList() => Column(
|
Widget _buildPhoneList() => _buildDynamicList(
|
||||||
children: List.generate(phoneControllers.length, (index) {
|
ctrls: phoneControllers,
|
||||||
return Padding(
|
labels: phoneLabels,
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
labelOptions: ["Work", "Mobile", "Other"],
|
||||||
child: _buildLabeledRow(
|
label: "Phone Label",
|
||||||
"Phone Label",
|
inputLabel: "Phone",
|
||||||
phoneLabels[index],
|
inputType: TextInputType.phone,
|
||||||
["Work", "Mobile", "Other"],
|
listToRemoveFrom: phoneControllers,
|
||||||
"Phone",
|
phoneSuffixIcon: const Icon(Icons.contact_phone, color: Colors.blue),
|
||||||
phoneControllers[index],
|
|
||||||
TextInputType.phone,
|
|
||||||
onRemove: phoneControllers.length > 1
|
|
||||||
? () {
|
|
||||||
phoneControllers.removeAt(index);
|
|
||||||
phoneLabels.removeAt(index);
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _popupSelector({
|
Widget _popupSelector({
|
||||||
required String hint,
|
required String hint,
|
||||||
required RxString selectedValue,
|
required RxString selectedValue,
|
||||||
required List<String> options,
|
required List<String> options,
|
||||||
}) {
|
}) =>
|
||||||
return Obx(() => GestureDetector(
|
Obx(() => GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final selected = await showMenu<String>(
|
final selected = await showMenu<String>(
|
||||||
context: context,
|
context: context,
|
||||||
position: RelativeRect.fromLTRB(100, 300, 100, 0),
|
position: RelativeRect.fromLTRB(100, 300, 100, 0),
|
||||||
items: options.map((option) {
|
items: options.map((option) => PopupMenuItem<String>(value: option, child: Text(option))).toList(),
|
||||||
return PopupMenuItem<String>(
|
|
||||||
value: option,
|
|
||||||
child: Text(option),
|
|
||||||
);
|
);
|
||||||
}).toList(),
|
if (selected != null) selectedValue.value = selected;
|
||||||
);
|
|
||||||
|
|
||||||
if (selected != null) {
|
|
||||||
selectedValue.value = selected;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 48,
|
height: 48,
|
||||||
@ -321,7 +317,6 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
|
||||||
|
|
||||||
Widget _sectionLabel(String title) => Column(
|
Widget _sectionLabel(String title) => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -332,6 +327,7 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// CHIP list for tags
|
||||||
Widget _tagInputSection() {
|
Widget _tagInputSection() {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -350,16 +346,14 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Obx(() => controller.filteredSuggestions.isEmpty
|
Obx(() => controller.filteredSuggestions.isEmpty
|
||||||
? const SizedBox()
|
? const SizedBox.shrink()
|
||||||
: Container(
|
: Container(
|
||||||
margin: const EdgeInsets.only(top: 4),
|
margin: const EdgeInsets.only(top: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
boxShadow: const [
|
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 4)],
|
||||||
BoxShadow(color: Colors.black12, blurRadius: 4)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
@ -392,9 +386,14 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTextField(String label, TextEditingController controller,
|
// ---- REQUIRED FIELD (reusable)
|
||||||
{int maxLines = 1}) {
|
Widget _buildTextField(
|
||||||
return Column(
|
String label,
|
||||||
|
TextEditingController controller, {
|
||||||
|
int maxLines = 1,
|
||||||
|
bool required = false,
|
||||||
|
}) =>
|
||||||
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MyText.labelMedium(label),
|
MyText.labelMedium(label),
|
||||||
@ -403,27 +402,29 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
controller: controller,
|
controller: controller,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
decoration: _inputDecoration("Enter $label"),
|
decoration: _inputDecoration("Enter $label"),
|
||||||
validator: (value) => value == null || value.trim().isEmpty
|
validator: required
|
||||||
? "$label is required"
|
? (value) =>
|
||||||
|
value == null || value.trim().isEmpty ? "$label is required" : null
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildOrganizationField() {
|
// -- Organization as required TextFormField
|
||||||
return Column(
|
Widget _buildOrganizationField() => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MyText.labelMedium("Organization"),
|
MyText.labelMedium("Organization"),
|
||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
TextField(
|
TextFormField(
|
||||||
controller: orgController,
|
controller: orgController,
|
||||||
onChanged: controller.filterOrganizationSuggestions,
|
onChanged: controller.filterOrganizationSuggestions,
|
||||||
decoration: _inputDecoration("Enter organization"),
|
decoration: _inputDecoration("Enter organization"),
|
||||||
|
validator: (value) =>
|
||||||
|
value == null || value.trim().isEmpty ? "Organization is required" : null,
|
||||||
),
|
),
|
||||||
Obx(() => controller.filteredOrgSuggestions.isEmpty
|
Obx(() => controller.filteredOrgSuggestions.isEmpty
|
||||||
? const SizedBox()
|
? const SizedBox.shrink()
|
||||||
: ListView.builder(
|
: ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemCount: controller.filteredOrgSuggestions.length,
|
itemCount: controller.filteredOrgSuggestions.length,
|
||||||
@ -441,10 +442,9 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildActionButtons() {
|
// Action button row
|
||||||
return Row(
|
Widget _buildActionButtons() => Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
@ -453,12 +453,10 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
Get.delete<AddContactController>();
|
Get.delete<AddContactController>();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.close, color: Colors.red),
|
icon: const Icon(Icons.close, color: Colors.red),
|
||||||
label:
|
label: MyText.bodyMedium("Cancel", color: Colors.red, fontWeight: 600),
|
||||||
MyText.bodyMedium("Cancel", color: Colors.red, fontWeight: 600),
|
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
side: const BorderSide(color: Colors.red),
|
side: const BorderSide(color: Colors.red),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
borderRadius: BorderRadius.circular(10)),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -467,7 +465,15 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (formKey.currentState!.validate()) {
|
// 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
|
final emails = emailControllers
|
||||||
.asMap()
|
.asMap()
|
||||||
.entries
|
.entries
|
||||||
@ -477,7 +483,6 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
"emailAddress": entry.value.text.trim(),
|
"emailAddress": entry.value.text.trim(),
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
final phones = phoneControllers
|
final phones = phoneControllers
|
||||||
.asMap()
|
.asMap()
|
||||||
.entries
|
.entries
|
||||||
@ -487,7 +492,6 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
"phoneNumber": entry.value.text.trim(),
|
"phoneNumber": entry.value.text.trim(),
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
controller.submitContact(
|
controller.submitContact(
|
||||||
id: widget.existingContact?.id,
|
id: widget.existingContact?.id,
|
||||||
name: nameController.text.trim(),
|
name: nameController.text.trim(),
|
||||||
@ -500,37 +504,115 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.check_circle_outline, color: Colors.white),
|
icon: const Icon(Icons.check_circle_outline, color: Colors.white),
|
||||||
label:
|
label: MyText.bodyMedium("Save", color: Colors.white, fontWeight: 600),
|
||||||
MyText.bodyMedium("Save", color: Colors.white, fontWeight: 600),
|
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.indigo,
|
backgroundColor: Colors.indigo,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
borderRadius: BorderRadius.circular(10)),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
if (!controller.isInitialized.value) {
|
if (!controller.isInitialized.value) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(top: 32).add(MediaQuery.of(context).viewInsets),
|
||||||
top: 32,
|
|
||||||
).add(MediaQuery.of(context).viewInsets),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).cardColor,
|
color: Theme.of(context).cardColor,
|
||||||
borderRadius:
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||||
const BorderRadius.vertical(top: Radius.circular(24)),
|
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
|
||||||
@ -541,37 +623,53 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
children: [
|
children: [
|
||||||
Center(
|
Center(
|
||||||
child: MyText.titleMedium(
|
child: MyText.titleMedium(
|
||||||
widget.existingContact != null
|
widget.existingContact != null ? "Edit Contact" : "Create New Contact",
|
||||||
? "Edit Contact"
|
|
||||||
: "Create New Contact",
|
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MySpacing.height(24),
|
MySpacing.height(24),
|
||||||
_sectionLabel("Required Fields"),
|
_sectionLabel("Required Fields"),
|
||||||
MySpacing.height(12),
|
MySpacing.height(12),
|
||||||
_buildTextField("Name", nameController),
|
_buildTextField("Name", nameController, required: true),
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
_buildOrganizationField(),
|
_buildOrganizationField(),
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
MyText.labelMedium("Select Bucket"),
|
MyText.labelMedium("Select Bucket"),
|
||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
_popupSelector(
|
_popupSelector(
|
||||||
hint: "Select Bucket",
|
hint: "Select Bucket",
|
||||||
selectedValue: controller.selectedBucket,
|
selectedValue: controller.selectedBucket,
|
||||||
options: controller.buckets,
|
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),
|
MySpacing.height(24),
|
||||||
Obx(() => GestureDetector(
|
Obx(() => GestureDetector(
|
||||||
onTap: () => showAdvanced.toggle(),
|
onTap: () => showAdvanced.toggle(),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
MyText.labelLarge("Advanced Details (Optional)",
|
MyText.labelLarge("Advanced Details (Optional)", fontWeight: 600),
|
||||||
fontWeight: 600),
|
Icon(showAdvanced.value ? Icons.expand_less : Icons.expand_more),
|
||||||
Icon(showAdvanced.value
|
|
||||||
? Icons.expand_less
|
|
||||||
: Icons.expand_more),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
@ -619,15 +717,12 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
|||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
_tagInputSection(),
|
_tagInputSection(),
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
_buildTextField("Address", addressController,
|
_buildTextField("Address", addressController, maxLines: 2, required: false),
|
||||||
maxLines: 2),
|
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
_buildTextField(
|
_buildTextField("Description", descriptionController, maxLines: 2, required: false),
|
||||||
"Description", descriptionController,
|
|
||||||
maxLines: 2),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: const SizedBox()),
|
: const SizedBox.shrink()),
|
||||||
MySpacing.height(24),
|
MySpacing.height(24),
|
||||||
_buildActionButtons(),
|
_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/helpers/utils/date_time_utils.dart';
|
||||||
import 'package:marco/model/directory/add_contact_bottom_sheet.dart';
|
import 'package:marco/model/directory/add_contact_bottom_sheet.dart';
|
||||||
|
|
||||||
class ContactDetailScreen extends StatefulWidget {
|
// HELPER: Delta to HTML conversion
|
||||||
final ContactModel contact;
|
|
||||||
|
|
||||||
const ContactDetailScreen({super.key, required this.contact});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ContactDetailScreen> createState() => _ContactDetailScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
String _convertDeltaToHtml(dynamic delta) {
|
String _convertDeltaToHtml(dynamic delta) {
|
||||||
final buffer = StringBuffer();
|
final buffer = StringBuffer();
|
||||||
bool inList = false;
|
bool inList = false;
|
||||||
|
|
||||||
for (var op in delta.toList()) {
|
for (var op in delta.toList()) {
|
||||||
final data = op.data?.toString() ?? '';
|
final String data = op.data?.toString() ?? '';
|
||||||
final attr = op.attributes ?? {};
|
final attr = op.attributes ?? {};
|
||||||
|
final bool isListItem = attr.containsKey('list');
|
||||||
|
|
||||||
final isListItem = attr.containsKey('list');
|
|
||||||
|
|
||||||
// Start list
|
|
||||||
if (isListItem && !inList) {
|
if (isListItem && !inList) {
|
||||||
buffer.write('<ul>');
|
buffer.write('<ul>');
|
||||||
inList = true;
|
inList = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close list if we are not in list mode anymore
|
|
||||||
if (!isListItem && inList) {
|
if (!isListItem && inList) {
|
||||||
buffer.write('</ul>');
|
buffer.write('</ul>');
|
||||||
inList = false;
|
inList = false;
|
||||||
@ -49,15 +37,12 @@ String _convertDeltaToHtml(dynamic delta) {
|
|||||||
|
|
||||||
if (isListItem) buffer.write('<li>');
|
if (isListItem) buffer.write('<li>');
|
||||||
|
|
||||||
// Apply inline styles
|
|
||||||
if (attr.containsKey('bold')) buffer.write('<strong>');
|
if (attr.containsKey('bold')) buffer.write('<strong>');
|
||||||
if (attr.containsKey('italic')) buffer.write('<em>');
|
if (attr.containsKey('italic')) buffer.write('<em>');
|
||||||
if (attr.containsKey('underline')) buffer.write('<u>');
|
if (attr.containsKey('underline')) buffer.write('<u>');
|
||||||
if (attr.containsKey('strike')) buffer.write('<s>');
|
if (attr.containsKey('strike')) buffer.write('<s>');
|
||||||
if (attr.containsKey('link')) buffer.write('<a href="${attr['link']}">');
|
if (attr.containsKey('link')) buffer.write('<a href="${attr['link']}">');
|
||||||
|
|
||||||
buffer.write(data.replaceAll('\n', ''));
|
buffer.write(data.replaceAll('\n', ''));
|
||||||
|
|
||||||
if (attr.containsKey('link')) buffer.write('</a>');
|
if (attr.containsKey('link')) buffer.write('</a>');
|
||||||
if (attr.containsKey('strike')) buffer.write('</s>');
|
if (attr.containsKey('strike')) buffer.write('</s>');
|
||||||
if (attr.containsKey('underline')) buffer.write('</u>');
|
if (attr.containsKey('underline')) buffer.write('</u>');
|
||||||
@ -66,14 +51,21 @@ String _convertDeltaToHtml(dynamic delta) {
|
|||||||
|
|
||||||
if (isListItem)
|
if (isListItem)
|
||||||
buffer.write('</li>');
|
buffer.write('</li>');
|
||||||
else if (data.contains('\n')) buffer.write('<br>');
|
else if (data.contains('\n')) {
|
||||||
|
buffer.write('<br>');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inList) buffer.write('</ul>');
|
if (inList) buffer.write('</ul>');
|
||||||
|
|
||||||
return buffer.toString();
|
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> {
|
class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||||
late final DirectoryController directoryController;
|
late final DirectoryController directoryController;
|
||||||
late final ProjectController projectController;
|
late final ProjectController projectController;
|
||||||
@ -85,7 +77,6 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
directoryController = Get.find<DirectoryController>();
|
directoryController = Get.find<DirectoryController>();
|
||||||
projectController = Get.find<ProjectController>();
|
projectController = Get.find<ProjectController>();
|
||||||
contact = widget.contact;
|
contact = widget.contact;
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
directoryController.fetchCommentsForContact(contact.id);
|
directoryController.fetchCommentsForContact(contact.id);
|
||||||
});
|
});
|
||||||
@ -103,13 +94,12 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildSubHeader(),
|
_buildSubHeader(),
|
||||||
|
const Divider(height: 1, thickness: 0.5, color: Colors.grey),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TabBarView(
|
child: TabBarView(children: [
|
||||||
children: [
|
|
||||||
_buildDetailsTab(),
|
_buildDetailsTab(),
|
||||||
_buildCommentsTab(context),
|
_buildCommentsTab(context),
|
||||||
],
|
]),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -130,10 +120,8 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.arrow_back_ios_new,
|
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black, size: 20),
|
||||||
color: Colors.black, size: 20),
|
onPressed: () => Get.offAllNamed('/dashboard/directory-main-page'),
|
||||||
onPressed: () =>
|
|
||||||
Get.offAllNamed('/dashboard/directory-main-page'),
|
|
||||||
),
|
),
|
||||||
MySpacing.width(8),
|
MySpacing.width(8),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -141,30 +129,10 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
MyText.titleLarge('Contact Profile',
|
MyText.titleLarge('Contact Profile', fontWeight: 700, color: Colors.black),
|
||||||
fontWeight: 700, color: Colors.black),
|
|
||||||
MySpacing.height(2),
|
MySpacing.height(2),
|
||||||
GetBuilder<ProjectController>(
|
GetBuilder<ProjectController>(
|
||||||
builder: (projectController) {
|
builder: (p) => ProjectLabel(p.selectedProject?.name),
|
||||||
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],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -176,34 +144,26 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSubHeader() {
|
Widget _buildSubHeader() {
|
||||||
|
final firstName = contact.name.split(" ").first;
|
||||||
|
final lastName = contact.name.split(" ").length > 1 ? contact.name.split(" ").last : "";
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: MySpacing.xy(16, 12),
|
padding: MySpacing.xy(16, 12),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(children: [
|
||||||
children: [
|
Avatar(firstName: firstName, lastName: lastName, size: 35, backgroundColor: Colors.indigo),
|
||||||
Avatar(
|
|
||||||
firstName: contact.name.split(" ").first,
|
|
||||||
lastName: contact.name.split(" ").length > 1
|
|
||||||
? contact.name.split(" ").last
|
|
||||||
: "",
|
|
||||||
size: 35,
|
|
||||||
backgroundColor: Colors.indigo,
|
|
||||||
),
|
|
||||||
MySpacing.width(12),
|
MySpacing.width(12),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MyText.titleSmall(contact.name,
|
MyText.titleSmall(contact.name, fontWeight: 600, color: Colors.black),
|
||||||
fontWeight: 600, color: Colors.black),
|
|
||||||
MySpacing.height(2),
|
MySpacing.height(2),
|
||||||
MyText.bodySmall(contact.organization,
|
MyText.bodySmall(contact.organization, fontWeight: 500, color: Colors.grey[700]),
|
||||||
fontWeight: 500, color: Colors.grey[700]),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
]),
|
||||||
TabBar(
|
TabBar(
|
||||||
labelColor: Colors.red,
|
labelColor: Colors.red,
|
||||||
unselectedLabelColor: Colors.black,
|
unselectedLabelColor: Colors.black,
|
||||||
@ -226,33 +186,37 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDetailsTab() {
|
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 tags = contact.tags.map((e) => e.name).join(", ");
|
||||||
|
|
||||||
final bucketNames = contact.bucketIds
|
final bucketNames = contact.bucketIds
|
||||||
.map((id) => directoryController.contactBuckets
|
.map((id) => directoryController.contactBuckets
|
||||||
.firstWhereOrNull((b) => b.id == id)
|
.firstWhereOrNull((b) => b.id == id)
|
||||||
?.name)
|
?.name)
|
||||||
.whereType<String>()
|
.whereType<String>()
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
final projectNames = contact.projectIds?.map((id) =>
|
||||||
final projectNames = contact.projectIds
|
projectController.projects.firstWhereOrNull((p) => p.id == id)?.name).whereType<String>().join(", ") ?? "-";
|
||||||
?.map((id) => projectController.projects
|
|
||||||
.firstWhereOrNull((p) => p.id == id)
|
|
||||||
?.name)
|
|
||||||
.whereType<String>()
|
|
||||||
.join(", ") ??
|
|
||||||
"-";
|
|
||||||
|
|
||||||
final category = contact.contactCategory?.name ?? "-";
|
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(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
@ -261,28 +225,38 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MySpacing.height(12),
|
MySpacing.height(12),
|
||||||
|
// BASIC INFO CARD
|
||||||
_infoCard("Basic Info", [
|
_infoCard("Basic Info", [
|
||||||
_iconInfoRow(Icons.email, "Email", email,
|
multiRows(
|
||||||
onTap: () => LauncherUtils.launchEmail(email),
|
items: contact.contactEmails.map((e) => e.emailAddress).toList(),
|
||||||
onLongPress: () => LauncherUtils.copyToClipboard(email,
|
icon: Icons.email,
|
||||||
typeLabel: "Email")),
|
label: "Email",
|
||||||
_iconInfoRow(Icons.phone, "Phone", phone,
|
typeLabel: "Email",
|
||||||
onTap: () => LauncherUtils.launchPhone(phone),
|
onTap: (email) => LauncherUtils.launchEmail(email),
|
||||||
onLongPress: () => LauncherUtils.copyToClipboard(phone,
|
onLongPress: (email) => LauncherUtils.copyToClipboard(email, typeLabel: "Email"),
|
||||||
typeLabel: "Phone")),
|
),
|
||||||
|
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),
|
_iconInfoRow(Icons.location_on, "Address", contact.address),
|
||||||
]),
|
]),
|
||||||
|
// ORGANIZATION CARD
|
||||||
_infoCard("Organization", [
|
_infoCard("Organization", [
|
||||||
_iconInfoRow(
|
_iconInfoRow(Icons.business, "Organization", contact.organization),
|
||||||
Icons.business, "Organization", contact.organization),
|
|
||||||
_iconInfoRow(Icons.category, "Category", category),
|
_iconInfoRow(Icons.category, "Category", category),
|
||||||
]),
|
]),
|
||||||
|
// META INFO CARD
|
||||||
_infoCard("Meta Info", [
|
_infoCard("Meta Info", [
|
||||||
_iconInfoRow(Icons.label, "Tags", tags.isNotEmpty ? tags : "-"),
|
_iconInfoRow(Icons.label, "Tags", tags.isNotEmpty ? tags : "-"),
|
||||||
_iconInfoRow(Icons.folder_shared, "Contact Buckets",
|
_iconInfoRow(Icons.folder_shared, "Contact Buckets", bucketNames.isNotEmpty ? bucketNames : "-"),
|
||||||
bucketNames.isNotEmpty ? bucketNames : "-"),
|
|
||||||
_iconInfoRow(Icons.work_outline, "Projects", projectNames),
|
_iconInfoRow(Icons.work_outline, "Projects", projectNames),
|
||||||
]),
|
]),
|
||||||
|
// DESCRIPTION CARD
|
||||||
_infoCard("Description", [
|
_infoCard("Description", [
|
||||||
MySpacing.height(6),
|
MySpacing.height(6),
|
||||||
Align(
|
Align(
|
||||||
@ -294,7 +268,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
])
|
]),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -309,25 +283,17 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result == true) {
|
if (result == true) {
|
||||||
await directoryController.fetchContacts();
|
await directoryController.fetchContacts();
|
||||||
final updated =
|
final updated =
|
||||||
directoryController.allContacts.firstWhereOrNull(
|
directoryController.allContacts.firstWhereOrNull((c) => c.id == contact.id);
|
||||||
(c) => c.id == contact.id,
|
|
||||||
);
|
|
||||||
if (updated != null) {
|
if (updated != null) {
|
||||||
setState(() {
|
setState(() => contact = updated);
|
||||||
contact = updated;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.edit, color: Colors.white),
|
icon: const Icon(Icons.edit, color: Colors.white),
|
||||||
label: const Text(
|
label: const Text("Edit Contact", style: TextStyle(color: Colors.white)),
|
||||||
"Edit Contact",
|
|
||||||
style: TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -337,24 +303,17 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
Widget _buildCommentsTab(BuildContext context) {
|
Widget _buildCommentsTab(BuildContext context) {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
final contactId = contact.id;
|
final contactId = contact.id;
|
||||||
|
|
||||||
if (!directoryController.contactCommentsMap.containsKey(contactId)) {
|
if (!directoryController.contactCommentsMap.containsKey(contactId)) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
final comments = directoryController.getCommentsForContact(contactId).reversed.toList();
|
||||||
final comments = directoryController
|
|
||||||
.getCommentsForContact(contactId)
|
|
||||||
.reversed
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
final editingId = directoryController.editingCommentId.value;
|
final editingId = directoryController.editingCommentId.value;
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
comments.isEmpty
|
comments.isEmpty
|
||||||
? Center(
|
? Center(
|
||||||
child:
|
child: MyText.bodyLarge("No comments yet.", color: Colors.grey),
|
||||||
MyText.bodyLarge("No comments yet.", color: Colors.grey),
|
|
||||||
)
|
)
|
||||||
: Padding(
|
: Padding(
|
||||||
padding: MySpacing.xy(12, 12),
|
padding: MySpacing.xy(12, 12),
|
||||||
@ -362,21 +321,43 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
padding: const EdgeInsets.only(bottom: 100),
|
padding: const EdgeInsets.only(bottom: 100),
|
||||||
itemCount: comments.length,
|
itemCount: comments.length,
|
||||||
separatorBuilder: (_, __) => MySpacing.height(14),
|
separatorBuilder: (_, __) => MySpacing.height(14),
|
||||||
itemBuilder: (_, index) {
|
itemBuilder: (_, index) => _buildCommentItem(comments[index], editingId, contact.id),
|
||||||
final comment = comments[index];
|
),
|
||||||
final isEditing = editingId == comment.id;
|
),
|
||||||
|
if (editingId == null)
|
||||||
|
Positioned(
|
||||||
|
bottom: 20,
|
||||||
|
right: 20,
|
||||||
|
child: FloatingActionButton.extended(
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
onPressed: () async {
|
||||||
|
final result = await Get.bottomSheet(
|
||||||
|
AddCommentBottomSheet(contactId: contactId),
|
||||||
|
isScrollControlled: true,
|
||||||
|
);
|
||||||
|
if (result == true) {
|
||||||
|
await directoryController.fetchCommentsForContact(contactId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.add_comment, color: Colors.white),
|
||||||
|
label: const Text("Add Comment", style: TextStyle(color: Colors.white)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCommentItem(comment, editingId, contactId) {
|
||||||
|
final isEditing = editingId == comment.id;
|
||||||
final initials = comment.createdBy.firstName.isNotEmpty
|
final initials = comment.createdBy.firstName.isNotEmpty
|
||||||
? comment.createdBy.firstName[0].toUpperCase()
|
? comment.createdBy.firstName[0].toUpperCase()
|
||||||
: "?";
|
: "?";
|
||||||
|
|
||||||
final decodedDelta = HtmlToDelta().convert(comment.note);
|
final decodedDelta = HtmlToDelta().convert(comment.note);
|
||||||
|
|
||||||
final quillController = isEditing
|
final quillController = isEditing
|
||||||
? quill.QuillController(
|
? quill.QuillController(
|
||||||
document: quill.Document.fromDelta(decodedDelta),
|
document: quill.Document.fromDelta(decodedDelta),
|
||||||
selection: TextSelection.collapsed(
|
selection: TextSelection.collapsed(offset: decodedDelta.length),
|
||||||
offset: decodedDelta.length),
|
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@ -387,18 +368,10 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
color: isEditing ? Colors.indigo[50] : Colors.white,
|
color: isEditing ? Colors.indigo[50] : Colors.white,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isEditing
|
color: isEditing ? Colors.indigo : Colors.grey.shade300,
|
||||||
? Colors.indigo
|
|
||||||
: Colors.grey.shade300,
|
|
||||||
width: 1.2,
|
width: 1.2,
|
||||||
),
|
),
|
||||||
boxShadow: const [
|
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 4, offset: Offset(0, 2))],
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black12,
|
|
||||||
blurRadius: 4,
|
|
||||||
offset: Offset(0, 2),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -407,21 +380,14 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Avatar(
|
Avatar(firstName: initials, lastName: '', size: 36),
|
||||||
firstName: initials,
|
|
||||||
lastName: '',
|
|
||||||
size: 36),
|
|
||||||
MySpacing.width(12),
|
MySpacing.width(12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment:
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
MyText.bodyMedium(
|
MyText.bodyMedium("By: ${comment.createdBy.firstName}",
|
||||||
"By: ${comment.createdBy.firstName}",
|
fontWeight: 600, color: Colors.indigo[800]),
|
||||||
fontWeight: 600,
|
|
||||||
color: Colors.indigo[800],
|
|
||||||
),
|
|
||||||
MySpacing.height(4),
|
MySpacing.height(4),
|
||||||
MyText.bodySmall(
|
MyText.bodySmall(
|
||||||
DateTimeUtils.convertUtcToLocal(
|
DateTimeUtils.convertUtcToLocal(
|
||||||
@ -440,8 +406,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
color: Colors.indigo,
|
color: Colors.indigo,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
directoryController.editingCommentId.value =
|
directoryController.editingCommentId.value = isEditing ? null : comment.id;
|
||||||
isEditing ? null : comment.id;
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -450,26 +415,14 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
if (isEditing && quillController != null)
|
if (isEditing && quillController != null)
|
||||||
CommentEditorCard(
|
CommentEditorCard(
|
||||||
controller: quillController,
|
controller: quillController,
|
||||||
onCancel: () {
|
onCancel: () => directoryController.editingCommentId.value = null,
|
||||||
directoryController.editingCommentId.value =
|
onSave: (ctrl) async {
|
||||||
null;
|
final delta = ctrl.document.toDelta();
|
||||||
},
|
|
||||||
onSave: (controller) async {
|
|
||||||
final delta = controller.document.toDelta();
|
|
||||||
final htmlOutput = _convertDeltaToHtml(delta);
|
final htmlOutput = _convertDeltaToHtml(delta);
|
||||||
final updated =
|
final updated = comment.copyWith(note: htmlOutput);
|
||||||
comment.copyWith(note: htmlOutput);
|
await directoryController.updateComment(updated);
|
||||||
|
await directoryController.fetchCommentsForContact(contactId);
|
||||||
await directoryController
|
directoryController.editingCommentId.value = null;
|
||||||
.updateComment(updated);
|
|
||||||
|
|
||||||
// ✅ Re-fetch comments to get updated list
|
|
||||||
await directoryController
|
|
||||||
.fetchCommentsForContact(contactId);
|
|
||||||
|
|
||||||
// ✅ Exit editing mode
|
|
||||||
directoryController.editingCommentId.value =
|
|
||||||
null;
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
@ -487,59 +440,35 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Floating Action Button
|
|
||||||
if (directoryController.editingCommentId.value == null)
|
|
||||||
Positioned(
|
|
||||||
bottom: 20,
|
|
||||||
right: 20,
|
|
||||||
child: FloatingActionButton.extended(
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
onPressed: () async {
|
|
||||||
final result = await Get.bottomSheet(
|
|
||||||
AddCommentBottomSheet(contactId: contactId),
|
|
||||||
isScrollControlled: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result == true) {
|
|
||||||
await directoryController
|
|
||||||
.fetchCommentsForContact(contactId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.add_comment, color: Colors.white),
|
|
||||||
label: const Text(
|
|
||||||
"Add Comment",
|
|
||||||
style: TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _iconInfoRow(IconData icon, String label, String value,
|
Widget _iconInfoRow(
|
||||||
{VoidCallback? onTap, VoidCallback? onLongPress}) {
|
IconData? icon,
|
||||||
|
String label,
|
||||||
|
String value, {
|
||||||
|
VoidCallback? onTap,
|
||||||
|
VoidCallback? onLongPress,
|
||||||
|
}) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: MySpacing.y(8),
|
padding: MySpacing.y(2),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onLongPress,
|
onLongPress: onLongPress,
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
if (icon != null) ...[
|
||||||
Icon(icon, size: 22, color: Colors.indigo),
|
Icon(icon, size: 22, color: Colors.indigo),
|
||||||
MySpacing.width(12),
|
MySpacing.width(12),
|
||||||
|
] else
|
||||||
|
const SizedBox(width: 34),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MyText.bodySmall(label,
|
if (label.isNotEmpty)
|
||||||
fontWeight: 600, color: Colors.black87),
|
MyText.bodySmall(label, fontWeight: 600, color: Colors.black87),
|
||||||
MySpacing.height(2),
|
if (label.isNotEmpty) MySpacing.height(2),
|
||||||
MyText.bodyMedium(value, color: Colors.grey[800]),
|
MyText.bodyMedium(value, color: Colors.grey[800]),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -560,8 +489,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MyText.titleSmall(title,
|
MyText.titleSmall(title, fontWeight: 700, color: Colors.indigo[700]),
|
||||||
fontWeight: 700, color: Colors.indigo[700]),
|
|
||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
...children,
|
...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> {
|
class _DirectoryViewState extends State<DirectoryView> {
|
||||||
final DirectoryController controller = Get.find();
|
final DirectoryController controller = Get.find();
|
||||||
final TextEditingController searchController = TextEditingController();
|
final TextEditingController searchController = TextEditingController();
|
||||||
final PermissionController permissionController =
|
final PermissionController permissionController = Get.put(PermissionController());
|
||||||
Get.put(PermissionController());
|
|
||||||
|
|
||||||
Future<void> _refreshDirectory() async {
|
Future<void> _refreshDirectory() async {
|
||||||
try {
|
try {
|
||||||
@ -304,7 +303,6 @@ class _DirectoryViewState extends State<DirectoryView> {
|
|||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
builder: (_) => const CreateBucketBottomSheet(),
|
builder: (_) => const CreateBucketBottomSheet(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (created == true) {
|
if (created == true) {
|
||||||
await controller.fetchBuckets();
|
await controller.fetchBuckets();
|
||||||
}
|
}
|
||||||
@ -442,27 +440,27 @@ class _DirectoryViewState extends State<DirectoryView> {
|
|||||||
color: Colors.grey[700],
|
color: Colors.grey[700],
|
||||||
overflow: TextOverflow.ellipsis),
|
overflow: TextOverflow.ellipsis),
|
||||||
MySpacing.height(8),
|
MySpacing.height(8),
|
||||||
...contact.contactEmails.map((e) =>
|
|
||||||
|
// Show only the first email (if present)
|
||||||
|
if (contact.contactEmails.isNotEmpty)
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => LauncherUtils.launchEmail(
|
onTap: () => LauncherUtils.launchEmail(
|
||||||
e.emailAddress),
|
contact.contactEmails.first.emailAddress),
|
||||||
onLongPress: () =>
|
onLongPress: () =>
|
||||||
LauncherUtils.copyToClipboard(
|
LauncherUtils.copyToClipboard(
|
||||||
e.emailAddress,
|
contact.contactEmails.first.emailAddress,
|
||||||
typeLabel: 'Email'),
|
typeLabel: 'Email',
|
||||||
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding:
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
const EdgeInsets.only(bottom: 4),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.email_outlined,
|
const Icon(Icons.email_outlined,
|
||||||
size: 16, color: Colors.indigo),
|
size: 16, color: Colors.indigo),
|
||||||
MySpacing.width(4),
|
MySpacing.width(4),
|
||||||
ConstrainedBox(
|
Expanded(
|
||||||
constraints: const BoxConstraints(
|
|
||||||
maxWidth: 180),
|
|
||||||
child: MyText.labelSmall(
|
child: MyText.labelSmall(
|
||||||
e.emailAddress,
|
contact.contactEmails.first.emailAddress,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
color: Colors.indigo,
|
color: Colors.indigo,
|
||||||
decoration:
|
decoration:
|
||||||
@ -472,32 +470,39 @@ class _DirectoryViewState extends State<DirectoryView> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)),
|
),
|
||||||
...contact.contactPhones.map((p) => Padding(
|
|
||||||
|
// Show only the first phone (if present)
|
||||||
|
if (contact.contactPhones.isNotEmpty)
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
bottom: 8, top: 4),
|
bottom: 8, top: 4),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
Expanded(
|
||||||
onTap: () =>
|
child: GestureDetector(
|
||||||
LauncherUtils.launchPhone(
|
onTap: () => LauncherUtils
|
||||||
p.phoneNumber),
|
.launchPhone(contact
|
||||||
|
.contactPhones
|
||||||
|
.first
|
||||||
|
.phoneNumber),
|
||||||
onLongPress: () =>
|
onLongPress: () =>
|
||||||
LauncherUtils.copyToClipboard(
|
LauncherUtils.copyToClipboard(
|
||||||
p.phoneNumber,
|
contact.contactPhones.first
|
||||||
typeLabel: 'Phone'),
|
.phoneNumber,
|
||||||
|
typeLabel: 'Phone',
|
||||||
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.phone_outlined,
|
const Icon(
|
||||||
|
Icons.phone_outlined,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: Colors.indigo),
|
color: Colors.indigo),
|
||||||
MySpacing.width(4),
|
MySpacing.width(4),
|
||||||
ConstrainedBox(
|
Expanded(
|
||||||
constraints:
|
|
||||||
const BoxConstraints(
|
|
||||||
maxWidth: 140),
|
|
||||||
child: MyText.labelSmall(
|
child: MyText.labelSmall(
|
||||||
p.phoneNumber,
|
contact.contactPhones.first
|
||||||
|
.phoneNumber,
|
||||||
overflow:
|
overflow:
|
||||||
TextOverflow.ellipsis,
|
TextOverflow.ellipsis,
|
||||||
color: Colors.indigo,
|
color: Colors.indigo,
|
||||||
@ -508,19 +513,22 @@ class _DirectoryViewState extends State<DirectoryView> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
MySpacing.width(8),
|
MySpacing.width(8),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
LauncherUtils.launchWhatsApp(
|
LauncherUtils.launchWhatsApp(
|
||||||
p.phoneNumber),
|
contact.contactPhones.first
|
||||||
|
.phoneNumber),
|
||||||
child: const FaIcon(
|
child: const FaIcon(
|
||||||
FontAwesomeIcons.whatsapp,
|
FontAwesomeIcons.whatsapp,
|
||||||
color: Colors.green,
|
color: Colors.green,
|
||||||
size: 16),
|
size: 16,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)),
|
),
|
||||||
if (tags.isNotEmpty) ...[
|
if (tags.isNotEmpty) ...[
|
||||||
MySpacing.height(2),
|
MySpacing.height(2),
|
||||||
MyText.labelSmall(tags.join(', '),
|
MyText.labelSmall(tags.join(', '),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user