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) {
|
||||
notesList.insert(0, note);
|
||||
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
|
||||
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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
Loading…
x
Reference in New Issue
Block a user