feat: Add restore and delete functionality for comments with confirmation dialogs

This commit is contained in:
Vaibhav Surve 2025-09-29 17:51:13 +05:30
parent d6587931fa
commit 7f924ee533
4 changed files with 200 additions and 66 deletions

View File

@ -97,10 +97,13 @@ class DirectoryController extends GetxController {
} }
} }
Future<void> fetchCommentsForContact(String contactId) async { Future<void> fetchCommentsForContact(String contactId,
{bool active = true}) async {
try { try {
final data = await ApiService.getDirectoryComments(contactId); final data =
logSafe("Fetched comments for contact $contactId: $data"); await ApiService.getDirectoryComments(contactId, active: active);
logSafe(
"Fetched ${active ? 'active' : 'inactive'} comments for contact $contactId: $data");
final comments = final comments =
data?.map((e) => DirectoryComment.fromJson(e)).toList() ?? []; data?.map((e) => DirectoryComment.fromJson(e)).toList() ?? [];
@ -112,7 +115,8 @@ class DirectoryController extends GetxController {
contactCommentsMap[contactId]!.assignAll(comments); contactCommentsMap[contactId]!.assignAll(comments);
contactCommentsMap[contactId]?.refresh(); contactCommentsMap[contactId]?.refresh();
} catch (e) { } catch (e) {
logSafe("Error fetching comments for contact $contactId: $e", logSafe(
"Error fetching ${active ? 'active' : 'inactive'} comments for contact $contactId: $e",
level: LogLevel.error); level: LogLevel.error);
contactCommentsMap[contactId] ??= <DirectoryComment>[].obs; contactCommentsMap[contactId] ??= <DirectoryComment>[].obs;
@ -120,6 +124,80 @@ class DirectoryController extends GetxController {
} }
} }
/// 🗑 Delete a comment (soft delete)
Future<void> 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<void> 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<void> fetchBuckets() async { Future<void> fetchBuckets() async {
try { try {
final response = await ApiService.getContactBucketList(); final response = await ApiService.getContactBucketList();

View File

@ -109,7 +109,7 @@ class NotesController extends GetxController {
Future<void> restoreOrDeleteNote(NoteModel note, Future<void> restoreOrDeleteNote(NoteModel note,
{bool restore = true}) async { {bool restore = true}) async {
final action = restore ? "restore" : "delete"; // <-- declare here final action = restore ? "restore" : "delete";
try { try {
logSafe("Attempting to $action note id: ${note.id}"); logSafe("Attempting to $action note id: ${note.id}");

View File

@ -1761,15 +1761,19 @@ class ApiService {
return false; return false;
} }
static Future<List<dynamic>?> getDirectoryComments(String contactId) async { static Future<List<dynamic>?> getDirectoryComments(
final url = "${ApiEndpoints.getDirectoryNotes}/$contactId"; String contactId, {
final response = await _getRequest(url); bool active = true,
final data = response != null }) async {
? _parseResponse(response, label: 'Directory Comments') final url = "${ApiEndpoints.getDirectoryNotes}/$contactId?active=$active";
: null; 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<bool> updateContact( static Future<bool> updateContact(
String contactId, Map<String, dynamic> payload) async { String contactId, Map<String, dynamic> payload) async {

View File

@ -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/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';
import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
import 'package:marco/helpers/widgets/my_confirmation_dialog.dart';
// HELPER: Delta to HTML conversion // HELPER: Delta to HTML conversion
String _convertDeltaToHtml(dynamic delta) { String _convertDeltaToHtml(dynamic delta) {
@ -81,8 +82,11 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
projectController = Get.find<ProjectController>(); projectController = Get.find<ProjectController>();
contactRx = widget.contact.obs; contactRx = widget.contact.obs;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) async {
directoryController.fetchCommentsForContact(contactRx.value.id); 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 // Listen to controller's allContacts and update contact if changed
@ -340,51 +344,48 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
Widget _buildCommentsTab() { Widget _buildCommentsTab() {
return Obx(() { return Obx(() {
final contactId = contactRx.value.id; 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) .getCommentsForContact(contactId)
.reversed .where((c) => c.isActive)
.toList(); .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; final editingId = directoryController.editingCommentId.value;
if (comments.isEmpty) {
return Center(
child: MyText.bodyLarge("No comments yet.", color: Colors.grey),
);
}
return Stack( return Stack(
children: [ children: [
MyRefreshIndicator( MyRefreshIndicator(
onRefresh: () async { onRefresh: () async {
await directoryController.fetchCommentsForContact(contactId); await directoryController.fetchCommentsForContact(contactId,
active: true);
await directoryController.fetchCommentsForContact(contactId,
active: false);
}, },
child: comments.isEmpty child: Padding(
? ListView( padding: MySpacing.xy(12, 12),
physics: const AlwaysScrollableScrollPhysics(), child: ListView.separated(
children: [ physics: const AlwaysScrollableScrollPhysics(),
SizedBox( padding: const EdgeInsets.only(bottom: 100),
height: Get.height * 0.6, itemCount: comments.length,
child: Center( separatorBuilder: (_, __) => MySpacing.height(14),
child: MyText.bodyLarge( itemBuilder: (_, index) =>
"No comments yet.", _buildCommentItem(comments[index], editingId, contactId),
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,
),
),
),
), ),
if (editingId == null) if (editingId == null)
Positioned( Positioned(
@ -398,15 +399,15 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
isScrollControlled: true, isScrollControlled: true,
); );
if (result == true) { if (result == true) {
await directoryController await directoryController.fetchCommentsForContact(contactId,
.fetchCommentsForContact(contactId); active: true);
await directoryController.fetchCommentsForContact(contactId,
active: false);
} }
}, },
icon: const Icon(Icons.add_comment, color: Colors.white), icon: const Icon(Icons.add_comment, color: Colors.white),
label: const Text( label: const Text("Add Comment",
"Add Comment", style: TextStyle(color: Colors.white)),
style: TextStyle(color: Colors.white),
),
), ),
), ),
], ],
@ -466,16 +467,67 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
], ],
), ),
), ),
IconButton( // Action buttons
icon: Icon( Row(
isEditing ? Icons.close : Icons.edit, children: [
size: 20, if (!comment.isActive)
color: Colors.indigo, IconButton(
), icon: const Icon(Icons.restore,
onPressed: () { color: Colors.green, size: 20),
directoryController.editingCommentId.value = tooltip: "Restore",
isEditing ? null : comment.id; 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;
},
),
],
), ),
], ],
), ),