feat: Add restore and delete functionality for comments with confirmation dialogs
This commit is contained in:
parent
d6587931fa
commit
7f924ee533
@ -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();
|
||||||
|
@ -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}");
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user