From 445cd75e0347816157680ac7b652a15b6dddbbc3 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 7 Jul 2025 15:34:07 +0530 Subject: [PATCH] feat(contact): implement contact editing functionality and update API integration --- .../directory/add_contact_controller.dart | 35 ++- lib/helpers/services/api_endpoints.dart | 1 + lib/helpers/services/api_service.dart | 24 ++ .../directory/add_contact_bottom_sheet.dart | 244 +++++++++++------- lib/view/directory/contact_detail_screen.dart | 109 +++++--- 5 files changed, 263 insertions(+), 150 deletions(-) diff --git a/lib/controller/directory/add_contact_controller.dart b/lib/controller/directory/add_contact_controller.dart index 7a78231..aa3f79c 100644 --- a/lib/controller/directory/add_contact_controller.dart +++ b/lib/controller/directory/add_contact_controller.dart @@ -82,6 +82,7 @@ class AddContactController extends GetxController { } Future submitContact({ + String? id, required String name, required String organization, required List> emails, @@ -96,10 +97,13 @@ class AddContactController extends GetxController { final tagObjects = enteredTags.map((tagName) { final tagId = tagsMap[tagName]; - return tagId != null ? {"id": tagId, "name": tagName} : {"name": tagName}; + return tagId != null + ? {"id": tagId, "name": tagName} + : {"name": tagName}; }).toList(); final body = { + if (id != null) "id": id, "name": name, "organization": organization, "contactCategoryId": categoryId, @@ -112,27 +116,30 @@ class AddContactController extends GetxController { "description": description, }; - logSafe("Submitting contact", sensitive: true); + logSafe("${id != null ? 'Updating' : 'Creating'} contact"); + + final response = id != null + ? await ApiService.updateContact(id, body) + : await ApiService.createContact(body); - final response = await ApiService.createContact(body); if (response == true) { - logSafe("Contact creation succeeded"); Get.back(result: true); showAppSnackbar( title: "Success", - message: "Contact created successfully", + message: id != null + ? "Contact updated successfully" + : "Contact created successfully", type: SnackbarType.success, ); } else { - logSafe("Contact creation failed", level: LogLevel.error); showAppSnackbar( title: "Error", - message: "Failed to create contact", + message: "Failed to ${id != null ? 'update' : 'create'} contact", type: SnackbarType.error, ); } } catch (e) { - logSafe("Contact creation error: \$e", level: LogLevel.error); + logSafe("Submit contact error: $e", level: LogLevel.error); showAppSnackbar( title: "Error", message: "Something went wrong", @@ -149,9 +156,12 @@ class AddContactController extends GetxController { final lower = query.toLowerCase(); filteredOrgSuggestions.assignAll( - organizationNames.where((name) => name.toLowerCase().contains(lower)).toList(), + organizationNames + .where((name) => name.toLowerCase().contains(lower)) + .toList(), ); - logSafe("Filtered organization suggestions for: \$query", level: LogLevel.debug); + logSafe("Filtered organization suggestions for: \$query", + level: LogLevel.debug); } Future fetchGlobalProjects() async { @@ -197,7 +207,10 @@ class AddContactController extends GetxController { final lower = query.toLowerCase(); filteredSuggestions.assignAll( - tags.where((tag) => tag.toLowerCase().contains(lower) && !enteredTags.contains(tag)).toList(), + tags + .where((tag) => + tag.toLowerCase().contains(lower) && !enteredTags.contains(tag)) + .toList(), ); logSafe("Filtered tag suggestions for: \$query", level: LogLevel.debug); } diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index cfda4f8..f0de389 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -40,6 +40,7 @@ class ApiEndpoints { static const String getDirectoryContactTags = "/master/contact-tags"; static const String getDirectoryOrganization = "/directory/organization"; static const String createContact = "/directory"; + static const String updateContact = "/directory"; static const String getDirectoryNotes = "/directory/notes"; static const String updateDirectoryNotes = "/directory/note"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index e9fcc82..f90df9f 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -365,6 +365,30 @@ class ApiService { return data is List ? data : null; } + static Future updateContact( + String contactId, Map payload) async { + try { + final endpoint = "${ApiEndpoints.updateContact}/$contactId"; + + logSafe("Updating contact [$contactId] with payload: $payload"); + + final response = await _putRequest(endpoint, payload); + if (response != null) { + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Contact updated successfully."); + return true; + } else { + logSafe("Update contact failed: ${json['message']}", + level: LogLevel.warning); + } + } + } catch (e) { + logSafe("Error updating contact: $e", level: LogLevel.error); + } + return false; + } + static Future createContact(Map payload) async { try { logSafe("Submitting contact payload: $payload"); diff --git a/lib/model/directory/add_contact_bottom_sheet.dart b/lib/model/directory/add_contact_bottom_sheet.dart index 3cbae01..92d81e2 100644 --- a/lib/model/directory/add_contact_bottom_sheet.dart +++ b/lib/model/directory/add_contact_bottom_sheet.dart @@ -1,26 +1,80 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:flutter/services.dart'; import 'package:marco/controller/directory/add_contact_controller.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text_style.dart'; -import 'package:flutter/services.dart'; +import 'package:marco/model/directory/contact_model.dart'; class AddContactBottomSheet extends StatelessWidget { - AddContactBottomSheet({super.key}) { + final ContactModel? existingContact; + + AddContactBottomSheet({super.key, this.existingContact}) { controller.resetForm(); - nameController.clear(); - orgController.clear(); + nameController.text = existingContact?.name ?? ''; + orgController.text = existingContact?.organization ?? ''; tagTextController.clear(); - addressController.clear(); - descriptionController.clear(); + addressController.text = existingContact?.address ?? ''; + descriptionController.text = existingContact?.description ?? ''; - emailControllers.add(TextEditingController()); - emailLabels.add('Office'.obs); + if (existingContact != null) { + emailControllers.clear(); + emailLabels.clear(); + for (var email in existingContact!.contactEmails) { + emailControllers.add(TextEditingController(text: email.emailAddress)); + emailLabels.add((email.label ?? 'Office').obs); + } - phoneControllers.add(TextEditingController()); - phoneLabels.add('Work'.obs); + if (emailControllers.isEmpty) { + emailControllers.add(TextEditingController()); + emailLabels.add('Office'.obs); + } + + phoneControllers.clear(); + phoneLabels.clear(); + for (var phone in existingContact!.contactPhones) { + phoneControllers.add(TextEditingController(text: phone.phoneNumber)); + phoneLabels.add((phone.label ?? 'Work').obs); + } + + if (phoneControllers.isEmpty) { + phoneControllers.add(TextEditingController()); + phoneLabels.add('Work'.obs); + } + + controller.selectedCategory.value = + existingContact!.contactCategory?.name ?? ''; + + if (existingContact!.projectIds?.isNotEmpty == true) { + controller.selectedProject.value = controller.globalProjects + .firstWhereOrNull( + (e) => e == existingContact!.projectIds!.first, + ) + ?.toString() ?? + ''; + } + + if (existingContact!.bucketIds.isNotEmpty) { + controller.selectedBucket.value = controller.buckets + .firstWhereOrNull( + (b) => b == existingContact!.bucketIds.first, + ) + ?.toString() ?? + ''; + } + + controller.enteredTags.assignAll( + existingContact!.tags.map((tag) => tag.name).toList(), + ); + } else { + emailControllers.add(TextEditingController()); + emailLabels.add('Office'.obs); + + phoneControllers.add(TextEditingController()); + phoneLabels.add('Work'.obs); + } } final controller = Get.put(AddContactController()); @@ -113,10 +167,9 @@ class AddContactBottomSheet extends StatelessWidget { MyText.labelMedium(label), MySpacing.height(8), _popupSelector( - hint: "Label", - selectedValue: selectedLabel, - options: options, - ), + hint: "Label", + selectedValue: selectedLabel, + options: options), ], ), ), @@ -128,40 +181,35 @@ class AddContactBottomSheet extends StatelessWidget { MyText.labelMedium(inputLabel), MySpacing.height(8), TextFormField( - controller: controller, - keyboardType: inputType, - maxLength: inputType == TextInputType.phone ? 10 : null, - inputFormatters: inputType == TextInputType.phone - ? [FilteringTextInputFormatter.digitsOnly] - : [], - decoration: _inputDecoration("Enter $inputLabel").copyWith( - counterText: "", // hides length indicator - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return "$inputLabel is required"; - } - - final trimmed = value.trim(); - - if (inputType == TextInputType.phone) { - if (!RegExp(r'^\d{10}$').hasMatch(trimmed)) { - return "Enter a valid 10-digit phone number"; - } - if (RegExp(r'^0+$').hasMatch(trimmed)) { - return "Phone number cannot be all zeroes"; - } - } - - if (inputType == TextInputType.emailAddress && - !RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(trimmed)) { - return "Enter a valid email address"; - } - - return null; - }, -), - + controller: controller, + keyboardType: inputType, + maxLength: inputType == TextInputType.phone ? 10 : null, + inputFormatters: inputType == TextInputType.phone + ? [FilteringTextInputFormatter.digitsOnly] + : [], + decoration: _inputDecoration("Enter $inputLabel") + .copyWith(counterText: ""), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return "$inputLabel is required"; + } + final trimmed = value.trim(); + if (inputType == TextInputType.phone) { + if (!RegExp(r'^\d{10}$').hasMatch(trimmed)) { + return "Enter a valid 10-digit phone number"; + } + if (RegExp(r'^0+$').hasMatch(trimmed)) { + return "Phone number cannot be all zeroes"; + } + } + if (inputType == TextInputType.emailAddress && + !RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$') + .hasMatch(trimmed)) { + return "Enter a valid email address"; + } + return null; + }, + ), ], ), ), @@ -177,53 +225,49 @@ class AddContactBottomSheet extends StatelessWidget { ); } - Widget _buildEmailList() { - return Column( - children: List.generate(emailControllers.length, (index) { - return Padding( - padding: const EdgeInsets.only(bottom: 12), - child: _buildLabeledRow( - "Email Label", - emailLabels[index], - ["Office", "Personal", "Other"], - "Email", - emailControllers[index], - TextInputType.emailAddress, - onRemove: emailControllers.length > 1 - ? () { - emailControllers.removeAt(index); - emailLabels.removeAt(index); - } - : null, - ), - ); - }), - ); - } + Widget _buildEmailList() => Column( + children: List.generate(emailControllers.length, (index) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _buildLabeledRow( + "Email Label", + emailLabels[index], + ["Office", "Personal", "Other"], + "Email", + emailControllers[index], + TextInputType.emailAddress, + onRemove: emailControllers.length > 1 + ? () { + emailControllers.removeAt(index); + emailLabels.removeAt(index); + } + : null, + ), + ); + }), + ); - Widget _buildPhoneList() { - return Column( - children: List.generate(phoneControllers.length, (index) { - return Padding( - padding: const EdgeInsets.only(bottom: 12), - child: _buildLabeledRow( - "Phone Label", - phoneLabels[index], - ["Work", "Mobile", "Other"], - "Phone", - phoneControllers[index], - TextInputType.phone, - onRemove: phoneControllers.length > 1 - ? () { - phoneControllers.removeAt(index); - phoneLabels.removeAt(index); - } - : null, - ), - ); - }), - ); - } + Widget _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 _dropdownField({ required String label, @@ -350,8 +394,13 @@ class AddContactBottomSheet extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( - child: MyText.titleMedium("Create New Contact", - fontWeight: 700)), + child: MyText.titleMedium( + existingContact != null + ? "Edit Contact" + : "Create New Contact", + fontWeight: 700, + ), + ), MySpacing.height(24), _sectionLabel("Basic Info"), MySpacing.height(16), @@ -519,8 +568,9 @@ class AddContactBottomSheet extends StatelessWidget { "phoneNumber": entry.value.text.trim(), }) .toList(); - + print("Submitting contact payload , id: ${existingContact?.id}"); controller.submitContact( + id: existingContact?.id, name: nameController.text.trim(), organization: orgController.text.trim(), emails: emails, diff --git a/lib/view/directory/contact_detail_screen.dart b/lib/view/directory/contact_detail_screen.dart index a8e9b4d..f6e5962 100644 --- a/lib/view/directory/contact_detail_screen.dart +++ b/lib/view/directory/contact_detail_screen.dart @@ -14,6 +14,7 @@ import 'package:marco/helpers/widgets/Directory/comment_editor_card.dart'; import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.dart'; import 'package:marco/model/directory/add_comment_bottom_sheet.dart'; import 'package:marco/helpers/utils/date_time_utils.dart'; +import 'package:marco/model/directory/add_contact_bottom_sheet.dart'; class ContactDetailScreen extends StatefulWidget { final ContactModel contact; @@ -252,47 +253,72 @@ class _ContactDetailScreenState extends State { final category = widget.contact.contactCategory?.name ?? "-"; - return SingleChildScrollView( - padding: MySpacing.xy(8, 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _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")), - _iconInfoRow(Icons.location_on, "Address", widget.contact.address), - ]), - _infoCard("Organization", [ - _iconInfoRow( - Icons.business, "Organization", widget.contact.organization), - _iconInfoRow(Icons.category, "Category", category), - ]), - _infoCard("Meta Info", [ - _iconInfoRow(Icons.label, "Tags", tags.isNotEmpty ? tags : "-"), - _iconInfoRow(Icons.folder_shared, "Contact Buckets", - bucketNames.isNotEmpty ? bucketNames : "-"), - _iconInfoRow(Icons.work_outline, "Projects", projectNames), - ]), - _infoCard("Description", [ - MySpacing.height(6), - Align( - alignment: Alignment.topLeft, - child: MyText.bodyMedium( - widget.contact.description, - color: Colors.grey[800], - maxLines: 10, - textAlign: TextAlign.left, - ), + return Stack( + children: [ + SingleChildScrollView( + padding: MySpacing.fromLTRB(8, 8, 8, 80), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MySpacing.height(12), + _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")), + _iconInfoRow( + Icons.location_on, "Address", widget.contact.address), + ]), + _infoCard("Organization", [ + _iconInfoRow(Icons.business, "Organization", + widget.contact.organization), + _iconInfoRow(Icons.category, "Category", category), + ]), + _infoCard("Meta Info", [ + _iconInfoRow(Icons.label, "Tags", tags.isNotEmpty ? tags : "-"), + _iconInfoRow(Icons.folder_shared, "Contact Buckets", + bucketNames.isNotEmpty ? bucketNames : "-"), + _iconInfoRow(Icons.work_outline, "Projects", projectNames), + ]), + _infoCard("Description", [ + MySpacing.height(6), + Align( + alignment: Alignment.topLeft, + child: MyText.bodyMedium( + widget.contact.description, + color: Colors.grey[800], + maxLines: 10, + textAlign: TextAlign.left, + ), + ), + ]) + ], + ), + ), + Positioned( + bottom: 20, + right: 20, + child: FloatingActionButton.extended( + backgroundColor: Colors.red, + onPressed: () { + Get.bottomSheet( + AddContactBottomSheet(existingContact: widget.contact), + isScrollControlled: true, + backgroundColor: Colors.transparent, + ); + }, + icon: const Icon(Icons.edit, color: Colors.white), + label: const Text( + "Edit Contact", + style: TextStyle(color: Colors.white), ), - ]) - ], - ), + ), + ), + ], ); } @@ -408,7 +434,6 @@ class _ContactDetailScreenState extends State { ), ], ), - MySpacing.height(12), // Comment Content if (isEditing && quillController != null) CommentEditorCard( @@ -453,7 +478,7 @@ class _ContactDetailScreenState extends State { bottom: 20, right: 20, child: FloatingActionButton.extended( - backgroundColor: Colors.indigo, + backgroundColor: Colors.red, onPressed: () { Get.bottomSheet( AddCommentBottomSheet(contactId: contactId),