feat: Implement restore and delete functionality for notes with confirmation dialog

This commit is contained in:
Vaibhav Surve 2025-09-29 17:03:47 +05:30
parent 8ad9690d89
commit d6587931fa
6 changed files with 281 additions and 79 deletions

View File

@ -107,6 +107,49 @@ class NotesController extends GetxController {
} }
} }
Future<void> restoreOrDeleteNote(NoteModel note,
{bool restore = true}) async {
final action = restore ? "restore" : "delete"; // <-- declare here
try {
logSafe("Attempting to $action note id: ${note.id}");
final success = await ApiService.restoreContactComment(
note.id,
restore, // true = restore, false = delete
);
if (success) {
final index = notesList.indexWhere((n) => n.id == note.id);
if (index != -1) {
notesList[index] = note.copyWith(isActive: restore);
notesList.refresh();
}
showAppSnackbar(
title: restore ? "Restored" : "Deleted",
message: restore
? "Note has been restored successfully."
: "Note has been deleted successfully.",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Error",
message:
restore ? "Failed to restore note." : "Failed to delete note.",
type: SnackbarType.error,
);
}
} catch (e, st) {
logSafe("$action note failed: $e", error: e, stackTrace: st);
showAppSnackbar(
title: "Error",
message: "Something went wrong while trying to $action the note.",
type: SnackbarType.error,
);
}
}
void addNote(NoteModel note) { void addNote(NoteModel note) {
notesList.insert(0, note); notesList.insert(0, note);
logSafe("Note added to list"); logSafe("Note added to list");

View File

@ -249,6 +249,46 @@ class ApiService {
} }
} }
static Future<http.Response?> _deleteRequest(
String endpoint, {
Map<String, String>? additionalHeaders,
Duration customTimeout = extendedTimeout,
bool hasRetried = false,
}) async {
String? token = await _getToken();
if (token == null) return null;
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint");
final headers = {
..._headers(token),
if (additionalHeaders != null) ...additionalHeaders,
};
logSafe("DELETE $uri\nHeaders: $headers");
try {
final response =
await http.delete(uri, headers: headers).timeout(customTimeout);
if (response.statusCode == 401 && !hasRetried) {
logSafe("Unauthorized DELETE. Attempting token refresh...");
if (await AuthService.refreshToken()) {
return await _deleteRequest(
endpoint,
additionalHeaders: additionalHeaders,
customTimeout: customTimeout,
hasRetried: true,
);
}
}
return response;
} catch (e) {
logSafe("HTTP DELETE Exception: $e", level: LogLevel.error);
return null;
}
}
/// Get Organizations assigned to a Project /// Get Organizations assigned to a Project
static Future<OrganizationListResponse?> getAssignedOrganizations( static Future<OrganizationListResponse?> getAssignedOrganizations(
String projectId) async { String projectId) async {
@ -1679,6 +1719,48 @@ class ApiService {
return false; return false;
} }
static Future<bool> restoreContactComment(
String commentId,
bool isActive,
) async {
final endpoint =
"${ApiEndpoints.updateDirectoryNotes}/$commentId?active=$isActive";
logSafe(
"Updating comment active status. commentId: $commentId, isActive: $isActive");
logSafe("Sending request to $endpoint ");
try {
final response = await _deleteRequest(
endpoint,
);
if (response == null) {
logSafe("Update comment failed: null response", level: LogLevel.error);
return false;
}
logSafe("Update comment response status: ${response.statusCode}");
logSafe("Update comment response body: ${response.body}");
final json = jsonDecode(response.body);
if (json['success'] == true) {
logSafe(
"Comment active status updated successfully. commentId: $commentId");
return true;
} else {
logSafe("Failed to update comment: ${json['message']}",
level: LogLevel.warning);
}
} catch (e, stack) {
logSafe("Exception during updateComment API: ${e.toString()}",
level: LogLevel.error);
logSafe("StackTrace: ${stack.toString()}", level: LogLevel.debug);
}
return false;
}
static Future<List<dynamic>?> getDirectoryComments(String contactId) async { static Future<List<dynamic>?> getDirectoryComments(String contactId) async {
final url = "${ApiEndpoints.getDirectoryNotes}/$contactId"; final url = "${ApiEndpoints.getDirectoryNotes}/$contactId";
final response = await _getRequest(url); final response = await _getRequest(url);
@ -1976,7 +2058,7 @@ class ApiService {
static Future<List<dynamic>?> getRoles() async => static Future<List<dynamic>?> getRoles() async =>
_getRequest(ApiEndpoints.getRoles).then( _getRequest(ApiEndpoints.getRoles).then(
(res) => res != null ? _parseResponse(res, label: 'Roles') : null); (res) => res != null ? _parseResponse(res, label: 'Roles') : null);
static Future<Map<String, dynamic>?> createEmployee({ static Future<Map<String, dynamic>?> createEmployee({
String? id, String? id,
required String firstName, required String firstName,
required String lastName, required String lastName,
@ -1987,7 +2069,7 @@ static Future<Map<String, dynamic>?> createEmployee({
String? email, String? email,
String? organizationId, String? organizationId,
bool? hasApplicationAccess, bool? hasApplicationAccess,
}) async { }) async {
final body = { final body = {
if (id != null) "id": id, if (id != null) "id": id,
"firstName": firstName, "firstName": firstName,
@ -1999,7 +2081,8 @@ static Future<Map<String, dynamic>?> createEmployee({
if (email != null && email.isNotEmpty) "email": email, if (email != null && email.isNotEmpty) "email": email,
if (organizationId != null && organizationId.isNotEmpty) if (organizationId != null && organizationId.isNotEmpty)
"organizationId": organizationId, "organizationId": organizationId,
if (hasApplicationAccess != null) "hasApplicationAccess": hasApplicationAccess, if (hasApplicationAccess != null)
"hasApplicationAccess": hasApplicationAccess,
}; };
final response = await _postRequest( final response = await _postRequest(
@ -2015,7 +2098,7 @@ static Future<Map<String, dynamic>?> createEmployee({
"success": response.statusCode == 200 && json['success'] == true, "success": response.statusCode == 200 && json['success'] == true,
"data": json, "data": json,
}; };
} }
static Future<Map<String, dynamic>?> getEmployeeDetails( static Future<Map<String, dynamic>?> getEmployeeDetails(
String employeeId) async { String employeeId) async {
@ -2029,14 +2112,14 @@ static Future<Map<String, dynamic>?> createEmployee({
// === Daily Task APIs === // === Daily Task APIs ===
static Future<List<dynamic>?> getDailyTasks( static Future<List<dynamic>?> getDailyTasks(
String projectId, { String projectId, {
DateTime? dateFrom, DateTime? dateFrom,
DateTime? dateTo, DateTime? dateTo,
List<String>? serviceIds, List<String>? serviceIds,
int pageNumber = 1, int pageNumber = 1,
int pageSize = 20, int pageSize = 20,
}) async { }) async {
final filterBody = { final filterBody = {
"serviceIds": serviceIds ?? [], "serviceIds": serviceIds ?? [],
}; };
@ -2051,13 +2134,15 @@ static Future<List<dynamic>?> getDailyTasks(
"filter": jsonEncode(filterBody), "filter": jsonEncode(filterBody),
}; };
final uri = Uri.parse(ApiEndpoints.getDailyTask).replace(queryParameters: query); final uri =
Uri.parse(ApiEndpoints.getDailyTask).replace(queryParameters: query);
final response = await _getRequest(uri.toString()); final response = await _getRequest(uri.toString());
return response != null ? _parseResponse(response, label: 'Daily Tasks') : null; return response != null
} ? _parseResponse(response, label: 'Daily Tasks')
: null;
}
static Future<bool> reportTask({ static Future<bool> reportTask({
required String id, required String id,

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
class ConfirmDialog extends StatelessWidget { class ConfirmDialog extends StatelessWidget {
final String title; final String title;
@ -115,7 +115,11 @@ class _ContentView extends StatelessWidget {
Navigator.pop(context, true); // close on success Navigator.pop(context, true); // close on success
} catch (e) { } catch (e) {
// Show error, dialog stays open // Show error, dialog stays open
Get.snackbar("Error", "Failed to delete. Try again."); showAppSnackbar(
title: "Error",
message: "Failed to delete. Try again.",
type: SnackbarType.error,
);
} finally { } finally {
loading.value = false; loading.value = false;
} }

View File

@ -79,7 +79,7 @@ class NoteModel {
required this.contactId, required this.contactId,
required this.isActive, required this.isActive,
}); });
NoteModel copyWith({String? note}) => NoteModel( NoteModel copyWith({String? note, bool? isActive}) => NoteModel(
id: id, id: id,
note: note ?? this.note, note: note ?? this.note,
contactName: contactName, contactName: contactName,
@ -89,7 +89,7 @@ class NoteModel {
updatedAt: updatedAt, updatedAt: updatedAt,
updatedBy: updatedBy, updatedBy: updatedBy,
contactId: contactId, contactId: contactId,
isActive: isActive, isActive: isActive ?? this.isActive,
); );
factory NoteModel.fromJson(Map<String, dynamic> json) { factory NoteModel.fromJson(Map<String, dynamic> json) {

View File

@ -11,6 +11,7 @@ import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/helpers/utils/date_time_utils.dart'; import 'package:marco/helpers/utils/date_time_utils.dart';
import 'package:marco/helpers/widgets/Directory/comment_editor_card.dart'; import 'package:marco/helpers/widgets/Directory/comment_editor_card.dart';
import 'package:marco/helpers/widgets/my_confirmation_dialog.dart';
class NotesView extends StatelessWidget { class NotesView extends StatelessWidget {
final NotesController controller = Get.find(); final NotesController controller = Get.find();
@ -244,17 +245,83 @@ class NotesView extends StatelessWidget {
], ],
), ),
), ),
/// Edit / Delete / Restore Icons
if (!note.isActive)
IconButton(
icon: const Icon(Icons.restore,
color: Colors.green, size: 20),
tooltip: "Restore",
padding: EdgeInsets
.zero,
onPressed: () async {
await Get.dialog(
ConfirmDialog(
title: "Restore Note",
message:
"Are you sure you want to restore this note?",
confirmText: "Restore",
confirmColor: Colors.green,
icon: Icons.restore,
onConfirm: () async {
await controller.restoreOrDeleteNote(
note,
restore: true);
},
),
barrierDismissible: false,
);
},
)
else
Row(
mainAxisSize: MainAxisSize.min,
children: [
/// Edit Icon
IconButton( IconButton(
icon: Icon( icon: Icon(
isEditing ? Icons.close : Icons.edit, isEditing ? Icons.close : Icons.edit,
color: Colors.indigo, color: Colors.indigo,
size: 20, size: 20,
), ),
padding: EdgeInsets
.zero,
constraints:
const BoxConstraints(),
onPressed: () { onPressed: () {
controller.editingNoteId.value = controller.editingNoteId.value =
isEditing ? null : note.id; isEditing ? null : note.id;
}, },
), ),
const SizedBox(
width: 6),
/// Delete Icon
IconButton(
icon: const Icon(Icons.delete_outline,
color: Colors.redAccent, size: 20),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () async {
await Get.dialog(
ConfirmDialog(
title: "Delete Note",
message:
"Are you sure you want to delete this note?",
confirmText: "Delete",
confirmColor: Colors.redAccent,
icon: Icons.delete_forever,
onConfirm: () async {
await controller
.restoreOrDeleteNote(note,
restore: false);
},
),
barrierDismissible: false,
);
},
),
],
),
], ],
), ),

View File

@ -624,8 +624,11 @@ class _UserDocumentsPageState extends State<UserDocumentsPage> {
reset: true, reset: true,
); );
} else { } else {
Get.snackbar( showAppSnackbar(
"Error", "Upload failed, please try again"); title: "Error",
message: "Upload failed, please try again",
type: SnackbarType.error,
);
} }
}, },
), ),