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) {
notesList.insert(0, note);
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
static Future<OrganizationListResponse?> getAssignedOrganizations(
String projectId) async {
@ -1679,6 +1719,48 @@ class ApiService {
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 {
final url = "${ApiEndpoints.getDirectoryNotes}/$contactId";
final response = await _getRequest(url);
@ -1976,46 +2058,47 @@ class ApiService {
static Future<List<dynamic>?> getRoles() async =>
_getRequest(ApiEndpoints.getRoles).then(
(res) => res != null ? _parseResponse(res, label: 'Roles') : null);
static Future<Map<String, dynamic>?> 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<Map<String, dynamic>?> 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<Map<String, dynamic>?> getEmployeeDetails(
String employeeId) async {
@ -2029,35 +2112,37 @@ static Future<Map<String, dynamic>?> createEmployee({
// === Daily Task APIs ===
static Future<List<dynamic>?> getDailyTasks(
String projectId, {
DateTime? dateFrom,
DateTime? dateTo,
List<String>? serviceIds,
int pageNumber = 1,
int pageSize = 20,
}) async {
final filterBody = {
"serviceIds": serviceIds ?? [],
};
static Future<List<dynamic>?> getDailyTasks(
String projectId, {
DateTime? dateFrom,
DateTime? dateTo,
List<String>? 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<bool> reportTask({
required String id,

View File

@ -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;
}

View File

@ -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<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/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;
},
),
],
),

View File

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