diff --git a/lib/controller/directory/notes_controller.dart b/lib/controller/directory/notes_controller.dart index 709b4e0..ab73cc7 100644 --- a/lib/controller/directory/notes_controller.dart +++ b/lib/controller/directory/notes_controller.dart @@ -107,6 +107,49 @@ class NotesController extends GetxController { } } + Future 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) { notesList.insert(0, note); logSafe("Note added to list"); diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 03cbb46..83b3b16 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -249,6 +249,46 @@ class ApiService { } } + static Future _deleteRequest( + String endpoint, { + Map? 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 static Future getAssignedOrganizations( String projectId) async { @@ -1679,6 +1719,48 @@ class ApiService { return false; } + static Future 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?> getDirectoryComments(String contactId) async { final url = "${ApiEndpoints.getDirectoryNotes}/$contactId"; final response = await _getRequest(url); @@ -1976,46 +2058,47 @@ class ApiService { static Future?> getRoles() async => _getRequest(ApiEndpoints.getRoles).then( (res) => res != null ? _parseResponse(res, label: 'Roles') : null); -static Future?> createEmployee({ - String? id, - required String firstName, - required String lastName, - required String phoneNumber, - required String gender, - required String jobRoleId, - required String joiningDate, - String? email, - String? organizationId, - bool? hasApplicationAccess, -}) async { - final body = { - if (id != null) "id": id, - "firstName": firstName, - "lastName": lastName, - "phoneNumber": phoneNumber, - "gender": gender, - "jobRoleId": jobRoleId, - "joiningDate": joiningDate, - if (email != null && email.isNotEmpty) "email": email, - if (organizationId != null && organizationId.isNotEmpty) - "organizationId": organizationId, - if (hasApplicationAccess != null) "hasApplicationAccess": hasApplicationAccess, - }; + static Future?> createEmployee({ + String? id, + required String firstName, + required String lastName, + required String phoneNumber, + required String gender, + required String jobRoleId, + required String joiningDate, + String? email, + String? organizationId, + bool? hasApplicationAccess, + }) async { + final body = { + if (id != null) "id": id, + "firstName": firstName, + "lastName": lastName, + "phoneNumber": phoneNumber, + "gender": gender, + "jobRoleId": jobRoleId, + "joiningDate": joiningDate, + if (email != null && email.isNotEmpty) "email": email, + if (organizationId != null && organizationId.isNotEmpty) + "organizationId": organizationId, + if (hasApplicationAccess != null) + "hasApplicationAccess": hasApplicationAccess, + }; - final response = await _postRequest( - ApiEndpoints.createEmployee, - body, - customTimeout: extendedTimeout, - ); + final response = await _postRequest( + ApiEndpoints.createEmployee, + body, + customTimeout: extendedTimeout, + ); - if (response == null) return null; + if (response == null) return null; - final json = jsonDecode(response.body); - return { - "success": response.statusCode == 200 && json['success'] == true, - "data": json, - }; -} + final json = jsonDecode(response.body); + return { + "success": response.statusCode == 200 && json['success'] == true, + "data": json, + }; + } static Future?> getEmployeeDetails( String employeeId) async { @@ -2029,35 +2112,37 @@ static Future?> createEmployee({ // === Daily Task APIs === -static Future?> getDailyTasks( - String projectId, { - DateTime? dateFrom, - DateTime? dateTo, - List? serviceIds, - int pageNumber = 1, - int pageSize = 20, -}) async { - final filterBody = { - "serviceIds": serviceIds ?? [], - }; + static Future?> getDailyTasks( + String projectId, { + DateTime? dateFrom, + DateTime? dateTo, + List? serviceIds, + int pageNumber = 1, + int pageSize = 20, + }) async { + final filterBody = { + "serviceIds": serviceIds ?? [], + }; - final query = { - "projectId": projectId, - "pageNumber": pageNumber.toString(), - "pageSize": pageSize.toString(), - if (dateFrom != null) - "dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom), - if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo), - "filter": jsonEncode(filterBody), - }; + final query = { + "projectId": projectId, + "pageNumber": pageNumber.toString(), + "pageSize": pageSize.toString(), + if (dateFrom != null) + "dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom), + if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo), + "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()); - - return response != null ? _parseResponse(response, label: 'Daily Tasks') : null; -} + final response = await _getRequest(uri.toString()); + return response != null + ? _parseResponse(response, label: 'Daily Tasks') + : null; + } static Future reportTask({ required String id, diff --git a/lib/helpers/widgets/my_confirmation_dialog.dart b/lib/helpers/widgets/my_confirmation_dialog.dart index 9c9c862..28dfa60 100644 --- a/lib/helpers/widgets/my_confirmation_dialog.dart +++ b/lib/helpers/widgets/my_confirmation_dialog.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/helpers/widgets/my_text.dart'; - +import 'package:marco/helpers/widgets/my_snackbar.dart'; class ConfirmDialog extends StatelessWidget { final String title; @@ -115,7 +115,11 @@ class _ContentView extends StatelessWidget { Navigator.pop(context, true); // close on success } catch (e) { // 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 { loading.value = false; } diff --git a/lib/model/directory/note_list_response_model.dart b/lib/model/directory/note_list_response_model.dart index e92283c..9cb1f2d 100644 --- a/lib/model/directory/note_list_response_model.dart +++ b/lib/model/directory/note_list_response_model.dart @@ -79,7 +79,7 @@ class NoteModel { required this.contactId, required this.isActive, }); - NoteModel copyWith({String? note}) => NoteModel( + NoteModel copyWith({String? note, bool? isActive}) => NoteModel( id: id, note: note ?? this.note, contactName: contactName, @@ -89,7 +89,7 @@ class NoteModel { updatedAt: updatedAt, updatedBy: updatedBy, contactId: contactId, - isActive: isActive, + isActive: isActive ?? this.isActive, ); factory NoteModel.fromJson(Map json) { diff --git a/lib/view/directory/notes_view.dart b/lib/view/directory/notes_view.dart index 2564584..501bab2 100644 --- a/lib/view/directory/notes_view.dart +++ b/lib/view/directory/notes_view.dart @@ -11,6 +11,7 @@ import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/avatar.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/my_confirmation_dialog.dart'; class NotesView extends StatelessWidget { final NotesController controller = Get.find(); @@ -244,17 +245,83 @@ class NotesView extends StatelessWidget { ], ), ), - IconButton( - icon: Icon( - isEditing ? Icons.close : Icons.edit, - color: Colors.indigo, - size: 20, + + /// 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( + 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; - }, - ), ], ), diff --git a/lib/view/document/user_document_screen.dart b/lib/view/document/user_document_screen.dart index 7eb581a..a60c73e 100644 --- a/lib/view/document/user_document_screen.dart +++ b/lib/view/document/user_document_screen.dart @@ -79,7 +79,7 @@ class _UserDocumentsPageState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (showDateHeader) + if (showDateHeader) Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: MyText.bodySmall( @@ -624,8 +624,11 @@ class _UserDocumentsPageState extends State { reset: true, ); } else { - Get.snackbar( - "Error", "Upload failed, please try again"); + showAppSnackbar( + title: "Error", + message: "Upload failed, please try again", + type: SnackbarType.error, + ); } }, ),