diff --git a/lib/controller/directory/directory_controller.dart b/lib/controller/directory/directory_controller.dart index 4b1e887..8de4291 100644 --- a/lib/controller/directory/directory_controller.dart +++ b/lib/controller/directory/directory_controller.dart @@ -97,10 +97,13 @@ class DirectoryController extends GetxController { } } - Future fetchCommentsForContact(String contactId) async { + Future fetchCommentsForContact(String contactId, + {bool active = true}) async { try { - final data = await ApiService.getDirectoryComments(contactId); - logSafe("Fetched comments for contact $contactId: $data"); + final data = + await ApiService.getDirectoryComments(contactId, active: active); + logSafe( + "Fetched ${active ? 'active' : 'inactive'} comments for contact $contactId: $data"); final comments = data?.map((e) => DirectoryComment.fromJson(e)).toList() ?? []; @@ -112,7 +115,8 @@ class DirectoryController extends GetxController { contactCommentsMap[contactId]!.assignAll(comments); contactCommentsMap[contactId]?.refresh(); } catch (e) { - logSafe("Error fetching comments for contact $contactId: $e", + logSafe( + "Error fetching ${active ? 'active' : 'inactive'} comments for contact $contactId: $e", level: LogLevel.error); contactCommentsMap[contactId] ??= [].obs; @@ -120,6 +124,80 @@ class DirectoryController extends GetxController { } } + /// 🗑️ Delete a comment (soft delete) + Future deleteComment(String commentId, String contactId) async { + try { + logSafe("Deleting comment. id: $commentId"); + + final success = await ApiService.restoreContactComment(commentId, false); + + if (success) { + logSafe("Comment deleted successfully. id: $commentId"); + + // Refresh comments after deletion + await fetchCommentsForContact(contactId); + + showAppSnackbar( + title: "Deleted", + message: "Comment deleted successfully.", + type: SnackbarType.success, + ); + } else { + logSafe("Failed to delete comment via API. id: $commentId"); + showAppSnackbar( + title: "Error", + message: "Failed to delete comment.", + type: SnackbarType.error, + ); + } + } catch (e, stack) { + logSafe("Delete comment failed: ${e.toString()}", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + showAppSnackbar( + title: "Error", + message: "Something went wrong while deleting comment.", + type: SnackbarType.error, + ); + } + } + + /// ♻️ Restore a previously deleted comment + Future restoreComment(String commentId, String contactId) async { + try { + logSafe("Restoring comment. id: $commentId"); + + final success = await ApiService.restoreContactComment(commentId, true); + + if (success) { + logSafe("Comment restored successfully. id: $commentId"); + + // Refresh comments after restore + await fetchCommentsForContact(contactId); + + showAppSnackbar( + title: "Restored", + message: "Comment restored successfully.", + type: SnackbarType.success, + ); + } else { + logSafe("Failed to restore comment via API. id: $commentId"); + showAppSnackbar( + title: "Error", + message: "Failed to restore comment.", + type: SnackbarType.error, + ); + } + } catch (e, stack) { + logSafe("Restore comment failed: ${e.toString()}", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + showAppSnackbar( + title: "Error", + message: "Something went wrong while restoring comment.", + type: SnackbarType.error, + ); + } + } + Future fetchBuckets() async { try { final response = await ApiService.getContactBucketList(); diff --git a/lib/controller/directory/notes_controller.dart b/lib/controller/directory/notes_controller.dart index ab73cc7..1868414 100644 --- a/lib/controller/directory/notes_controller.dart +++ b/lib/controller/directory/notes_controller.dart @@ -109,7 +109,7 @@ class NotesController extends GetxController { Future restoreOrDeleteNote(NoteModel note, {bool restore = true}) async { - final action = restore ? "restore" : "delete"; // <-- declare here + final action = restore ? "restore" : "delete"; try { logSafe("Attempting to $action note id: ${note.id}"); diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 83b3b16..1f15ef4 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -1761,15 +1761,19 @@ class ApiService { return false; } - static Future?> getDirectoryComments(String contactId) async { - final url = "${ApiEndpoints.getDirectoryNotes}/$contactId"; - final response = await _getRequest(url); - final data = response != null - ? _parseResponse(response, label: 'Directory Comments') - : null; +static Future?> getDirectoryComments( + String contactId, { + bool active = true, +}) async { + final url = "${ApiEndpoints.getDirectoryNotes}/$contactId?active=$active"; + final response = await _getRequest(url); + final data = response != null + ? _parseResponse(response, label: 'Directory Comments') + : null; + + return data is List ? data : null; +} - return data is List ? data : null; - } static Future updateContact( String contactId, Map payload) async { diff --git a/lib/view/directory/contact_detail_screen.dart b/lib/view/directory/contact_detail_screen.dart index b00856b..aed3d35 100644 --- a/lib/view/directory/contact_detail_screen.dart +++ b/lib/view/directory/contact_detail_screen.dart @@ -16,6 +16,7 @@ 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'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; +import 'package:marco/helpers/widgets/my_confirmation_dialog.dart'; // HELPER: Delta to HTML conversion String _convertDeltaToHtml(dynamic delta) { @@ -81,8 +82,11 @@ class _ContactDetailScreenState extends State { projectController = Get.find(); contactRx = widget.contact.obs; - WidgetsBinding.instance.addPostFrameCallback((_) { - directoryController.fetchCommentsForContact(contactRx.value.id); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await directoryController.fetchCommentsForContact(contactRx.value.id, + active: true); + await directoryController.fetchCommentsForContact(contactRx.value.id, + active: false); }); // Listen to controller's allContacts and update contact if changed @@ -340,51 +344,48 @@ class _ContactDetailScreenState extends State { Widget _buildCommentsTab() { return Obx(() { final contactId = contactRx.value.id; - if (!directoryController.contactCommentsMap.containsKey(contactId)) { - return const Center(child: CircularProgressIndicator()); - } - final comments = directoryController + // Get active and inactive comments + final activeComments = directoryController .getCommentsForContact(contactId) - .reversed + .where((c) => c.isActive) .toList(); + final inactiveComments = directoryController + .getCommentsForContact(contactId) + .where((c) => !c.isActive) + .toList(); + + // Combine both and keep the same sorting (recent first) + final comments = + [...activeComments, ...inactiveComments].reversed.toList(); final editingId = directoryController.editingCommentId.value; + if (comments.isEmpty) { + return Center( + child: MyText.bodyLarge("No comments yet.", color: Colors.grey), + ); + } + return Stack( children: [ MyRefreshIndicator( onRefresh: () async { - await directoryController.fetchCommentsForContact(contactId); + await directoryController.fetchCommentsForContact(contactId, + active: true); + await directoryController.fetchCommentsForContact(contactId, + active: false); }, - child: comments.isEmpty - ? ListView( - physics: const AlwaysScrollableScrollPhysics(), - children: [ - SizedBox( - height: Get.height * 0.6, - child: Center( - child: MyText.bodyLarge( - "No comments yet.", - color: Colors.grey, - ), - ), - ), - ], - ) - : Padding( - padding: MySpacing.xy(12, 12), - child: ListView.separated( - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.only(bottom: 100), - itemCount: comments.length, - separatorBuilder: (_, __) => MySpacing.height(14), - itemBuilder: (_, index) => _buildCommentItem( - comments[index], - editingId, - contactId, - ), - ), - ), + child: Padding( + padding: MySpacing.xy(12, 12), + child: ListView.separated( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.only(bottom: 100), + itemCount: comments.length, + separatorBuilder: (_, __) => MySpacing.height(14), + itemBuilder: (_, index) => + _buildCommentItem(comments[index], editingId, contactId), + ), + ), ), if (editingId == null) Positioned( @@ -398,15 +399,15 @@ class _ContactDetailScreenState extends State { isScrollControlled: true, ); if (result == true) { - await directoryController - .fetchCommentsForContact(contactId); + await directoryController.fetchCommentsForContact(contactId, + active: true); + await directoryController.fetchCommentsForContact(contactId, + active: false); } }, 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)), ), ), ], @@ -466,16 +467,67 @@ class _ContactDetailScreenState extends State { ], ), ), - IconButton( - icon: Icon( - isEditing ? Icons.close : Icons.edit, - size: 20, - color: Colors.indigo, - ), - onPressed: () { - directoryController.editingCommentId.value = - isEditing ? null : comment.id; - }, + // Action buttons + Row( + children: [ + if (!comment.isActive) + IconButton( + icon: const Icon(Icons.restore, + color: Colors.green, size: 20), + tooltip: "Restore", + onPressed: () async { + await Get.dialog( + ConfirmDialog( + title: "Restore Comment", + message: + "Are you sure you want to restore this comment?", + confirmText: "Restore", + confirmColor: Colors.green, + icon: Icons.restore, + onConfirm: () async { + await directoryController.restoreComment( + comment.id, contactId); + }, + ), + barrierDismissible: false, + ); + }, + ) + else + IconButton( + icon: + const Icon(Icons.delete, color: Colors.red, size: 20), + tooltip: "Delete", + onPressed: () async { + await Get.dialog( + ConfirmDialog( + title: "Delete Comment", + message: + "Are you sure you want to delete this comment?", + confirmText: "Delete", + confirmColor: Colors.red, + icon: Icons.delete_forever, + onConfirm: () async { + await directoryController.deleteComment( + comment.id, contactId); + }, + ), + barrierDismissible: false, + ); + }, + ), + IconButton( + icon: Icon( + isEditing ? Icons.close : Icons.edit, + size: 20, + color: Colors.indigo, + ), + onPressed: () { + directoryController.editingCommentId.value = + isEditing ? null : comment.id; + }, + ), + ], ), ], ),