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