import 'package:get/get.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/model/directory/contact_model.dart'; import 'package:marco/model/directory/contact_bucket_list_model.dart'; import 'package:marco/model/directory/directory_comment_model.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; class DirectoryController extends GetxController { RxList allContacts = [].obs; RxList filteredContacts = [].obs; RxList contactCategories = [].obs; RxList selectedCategories = [].obs; RxList selectedBuckets = [].obs; RxBool isActive = true.obs; RxBool isLoading = false.obs; RxList contactBuckets = [].obs; RxString searchQuery = ''.obs; RxBool showFabMenu = false.obs; final RxBool showFullEditorToolbar = false.obs; final RxBool isEditorFocused = false.obs; RxBool isNotesView = false.obs; final Map> contactCommentsMap = {}; RxList getCommentsForContact(String contactId) { return contactCommentsMap[contactId] ?? [].obs; } final editingCommentId = Rxn(); @override void onInit() { super.onInit(); fetchContacts(); fetchBuckets(); } // inside DirectoryController Future updateComment(DirectoryComment comment) async { try { logSafe( "Attempting to update comment. id: ${comment.id}, contactId: ${comment.contactId}"); final commentList = contactCommentsMap[comment.contactId]; final oldComment = commentList?.firstWhereOrNull((c) => c.id == comment.id); if (oldComment == null) { logSafe("Old comment not found. id: ${comment.id}"); } else { logSafe("Old comment note: ${oldComment.note}"); logSafe("New comment note: ${comment.note}"); } if (oldComment != null && oldComment.note.trim() == comment.note.trim()) { logSafe("No changes detected in comment. id: ${comment.id}"); showAppSnackbar( title: "No Changes", message: "No changes were made to the comment.", type: SnackbarType.info, ); return; } final success = await ApiService.updateContactComment( comment.id, comment.note, comment.contactId, ); if (success) { logSafe("Comment updated successfully. id: ${comment.id}"); await fetchCommentsForContact(comment.contactId); // ✅ Show success message showAppSnackbar( title: "Success", message: "Comment updated successfully.", type: SnackbarType.success, ); } else { logSafe("Failed to update comment via API. id: ${comment.id}"); showAppSnackbar( title: "Error", message: "Failed to update comment.", type: SnackbarType.error, ); } } catch (e, stackTrace) { logSafe("Update comment failed: ${e.toString()}"); logSafe("StackTrace: ${stackTrace.toString()}"); showAppSnackbar( title: "Error", message: "Failed to update comment.", type: SnackbarType.error, ); } } Future fetchCommentsForContact(String contactId, {bool active = true}) async { try { final data = await ApiService.getDirectoryComments(contactId, active: active); logSafe( "Fetched ${active ? 'active' : 'inactive'} comments for contact $contactId: $data"); final comments = data?.map((e) => DirectoryComment.fromJson(e)).toList() ?? []; if (!contactCommentsMap.containsKey(contactId)) { contactCommentsMap[contactId] = [].obs; } contactCommentsMap[contactId]!.assignAll(comments); contactCommentsMap[contactId]?.refresh(); } catch (e) { logSafe( "Error fetching ${active ? 'active' : 'inactive'} comments for contact $contactId: $e", level: LogLevel.error); contactCommentsMap[contactId] ??= [].obs; contactCommentsMap[contactId]!.clear(); } } /// 🗑️ Delete a comment (soft delete) Future deleteComment(String commentId, String contactId) async { try { logSafe("Deleting comment. id: $commentId"); final success = await ApiService.restoreContactComment(commentId, false); if (success) { logSafe("Comment deleted successfully. id: $commentId"); // Refresh comments after deletion await fetchCommentsForContact(contactId); showAppSnackbar( title: "Deleted", message: "Comment deleted successfully.", type: SnackbarType.success, ); } else { logSafe("Failed to delete comment via API. id: $commentId"); showAppSnackbar( title: "Error", message: "Failed to delete comment.", type: SnackbarType.error, ); } } catch (e, stack) { logSafe("Delete comment failed: ${e.toString()}", level: LogLevel.error); logSafe("StackTrace: $stack", level: LogLevel.debug); showAppSnackbar( title: "Error", message: "Something went wrong while deleting comment.", type: SnackbarType.error, ); } } /// ♻️ Restore a previously deleted comment Future restoreComment(String commentId, String contactId) async { try { logSafe("Restoring comment. id: $commentId"); final success = await ApiService.restoreContactComment(commentId, true); if (success) { logSafe("Comment restored successfully. id: $commentId"); // Refresh comments after restore await fetchCommentsForContact(contactId); showAppSnackbar( title: "Restored", message: "Comment restored successfully.", type: SnackbarType.success, ); } else { logSafe("Failed to restore comment via API. id: $commentId"); showAppSnackbar( title: "Error", message: "Failed to restore comment.", type: SnackbarType.error, ); } } catch (e, stack) { logSafe("Restore comment failed: ${e.toString()}", level: LogLevel.error); logSafe("StackTrace: $stack", level: LogLevel.debug); showAppSnackbar( title: "Error", message: "Something went wrong while restoring comment.", type: SnackbarType.error, ); } } Future fetchBuckets() async { try { final response = await ApiService.getContactBucketList(); if (response != null && response['data'] is List) { final buckets = (response['data'] as List) .map((e) => ContactBucket.fromJson(e)) .toList(); contactBuckets.assignAll(buckets); } else { contactBuckets.clear(); } } catch (e) { logSafe("Bucket fetch error: $e", level: LogLevel.error); } } Future fetchContacts({bool active = true}) async { try { isLoading.value = true; final response = await ApiService.getDirectoryData(isActive: active); if (response != null) { final contacts = response.map((e) => ContactModel.fromJson(e)).toList(); allContacts.assignAll(contacts); extractCategoriesFromContacts(); applyFilters(); } else { allContacts.clear(); filteredContacts.clear(); } } catch (e) { logSafe("Directory fetch error: $e", level: LogLevel.error); } finally { isLoading.value = false; } } void extractCategoriesFromContacts() { final uniqueCategories = {}; for (final contact in allContacts) { final category = contact.contactCategory; if (category != null && !uniqueCategories.containsKey(category.id)) { uniqueCategories[category.id] = category; } } contactCategories.value = uniqueCategories.values.toList(); } void applyFilters() { final query = searchQuery.value.toLowerCase(); filteredContacts.value = allContacts.where((contact) { final categoryMatch = selectedCategories.isEmpty || (contact.contactCategory != null && selectedCategories.contains(contact.contactCategory!.id)); final bucketMatch = selectedBuckets.isEmpty || contact.bucketIds.any((id) => selectedBuckets.contains(id)); final nameMatch = contact.name.toLowerCase().contains(query); final orgMatch = contact.organization.toLowerCase().contains(query); final emailMatch = contact.contactEmails .any((e) => e.emailAddress.toLowerCase().contains(query)); final phoneMatch = contact.contactPhones .any((p) => p.phoneNumber.toLowerCase().contains(query)); final tagMatch = contact.tags.any((tag) => tag.name.toLowerCase().contains(query)); final categoryNameMatch = contact.contactCategory?.name.toLowerCase().contains(query) ?? false; final bucketNameMatch = contact.bucketIds.any((id) { final bucketName = contactBuckets .firstWhereOrNull((b) => b.id == id) ?.name .toLowerCase() ?? ''; return bucketName.contains(query); }); final searchMatch = query.isEmpty || nameMatch || orgMatch || emailMatch || phoneMatch || tagMatch || categoryNameMatch || bucketNameMatch; return categoryMatch && bucketMatch && searchMatch; }).toList(); // 🔑 Ensure results are always alphabetically sorted filteredContacts .sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); } void toggleCategory(String categoryId) { if (selectedCategories.contains(categoryId)) { selectedCategories.remove(categoryId); } else { selectedCategories.add(categoryId); } } void toggleBucket(String bucketId) { if (selectedBuckets.contains(bucketId)) { selectedBuckets.remove(bucketId); } else { selectedBuckets.add(bucketId); } } void updateSearchQuery(String value) { searchQuery.value = value; applyFilters(); } String getBucketNames(ContactModel contact, List allBuckets) { return contact.bucketIds .map((id) => allBuckets.firstWhereOrNull((b) => b.id == id)?.name ?? '') .where((name) => name.isNotEmpty) .join(', '); } bool hasActiveFilters() { return selectedCategories.isNotEmpty || selectedBuckets.isNotEmpty || searchQuery.value.trim().isNotEmpty; } }