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,46 +2058,47 @@ 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,
required String phoneNumber, required String phoneNumber,
required String gender, required String gender,
required String jobRoleId, required String jobRoleId,
required String joiningDate, required String joiningDate,
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,
"lastName": lastName, "lastName": lastName,
"phoneNumber": phoneNumber, "phoneNumber": phoneNumber,
"gender": gender, "gender": gender,
"jobRoleId": jobRoleId, "jobRoleId": jobRoleId,
"joiningDate": joiningDate, "joiningDate": joiningDate,
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(
ApiEndpoints.createEmployee, ApiEndpoints.createEmployee,
body, body,
customTimeout: extendedTimeout, customTimeout: extendedTimeout,
); );
if (response == null) return null; if (response == null) return null;
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
return { return {
"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,35 +2112,37 @@ 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 ?? [],
}; };
final query = { final query = {
"projectId": projectId, "projectId": projectId,
"pageNumber": pageNumber.toString(), "pageNumber": pageNumber.toString(),
"pageSize": pageSize.toString(), "pageSize": pageSize.toString(),
if (dateFrom != null) if (dateFrom != null)
"dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom), "dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom),
if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo), if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo),
"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 {
], ],
), ),
), ),
IconButton(
icon: Icon( /// Edit / Delete / Restore Icons
isEditing ? Icons.close : Icons.edit, if (!note.isActive)
color: Colors.indigo, IconButton(
size: 20, 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(
icon: Icon(
isEditing ? Icons.close : Icons.edit,
color: Colors.indigo,
size: 20,
),
padding: EdgeInsets
.zero,
constraints:
const BoxConstraints(),
onPressed: () {
controller.editingNoteId.value =
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,
);
},
),
],
), ),
onPressed: () {
controller.editingNoteId.value =
isEditing ? null : note.id;
},
),
], ],
), ),

View File

@ -79,7 +79,7 @@ class _UserDocumentsPageState extends State<UserDocumentsPage> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (showDateHeader) if (showDateHeader)
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: MyText.bodySmall( child: MyText.bodySmall(
@ -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,
);
} }
}, },
), ),