import 'package:get/get.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/widgets/my_snackbar.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'; class DirectoryController extends GetxController { // -------------------- CONTACTS -------------------- 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; // -------------------- COMMENTS -------------------- final Map> activeCommentsMap = {}; final Map> inactiveCommentsMap = {}; final editingCommentId = Rxn(); @override void onInit() { super.onInit(); fetchContacts(); fetchBuckets(); } // -------------------- COMMENTS HANDLING -------------------- RxList getCommentsForContact(String contactId, {bool active = true}) { return active ? activeCommentsMap[contactId] ?? [].obs : inactiveCommentsMap[contactId] ?? [].obs; } Future fetchCommentsForContact(String contactId, {bool active = true}) async { try { final data = await ApiService.getDirectoryComments(contactId, active: active); var comments = data?.map((e) => DirectoryComment.fromJson(e)).toList() ?? []; // ✅ Deduplicate by ID before storing final Map uniqueMap = { for (var c in comments) c.id: c, }; comments = uniqueMap.values.toList() ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); if (active) { activeCommentsMap[contactId] = [].obs ..assignAll(comments); } else { inactiveCommentsMap[contactId] = [].obs ..assignAll(comments); } } catch (e, stack) { logSafe("Error fetching ${active ? 'active' : 'inactive'} comments: $e", level: LogLevel.error); logSafe(stack.toString(), level: LogLevel.debug); if (active) { activeCommentsMap[contactId] = [].obs; } else { inactiveCommentsMap[contactId] = [].obs; } } } List combinedComments(String contactId) { final activeList = getCommentsForContact(contactId, active: true); final inactiveList = getCommentsForContact(contactId, active: false); // ✅ Deduplicate by ID (active wins) final Map byId = {}; for (final c in inactiveList) { byId[c.id] = c; } for (final c in activeList) { byId[c.id] = c; } final combined = byId.values.toList() ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); return combined; } Future updateComment(DirectoryComment comment) async { try { final existing = getCommentsForContact(comment.contactId) .firstWhereOrNull((c) => c.id == comment.id); if (existing != null && existing.note.trim() == comment.note.trim()) { 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) { await fetchCommentsForContact(comment.contactId, active: true); await fetchCommentsForContact(comment.contactId, active: false); showAppSnackbar( title: "Success", message: "Comment updated successfully.", type: SnackbarType.success, ); } else { showAppSnackbar( title: "Error", message: "Failed to update comment.", type: SnackbarType.error, ); } } catch (e, stack) { logSafe("Update comment failed: $e", level: LogLevel.error); logSafe(stack.toString(), level: LogLevel.debug); showAppSnackbar( title: "Error", message: "Failed to update comment.", type: SnackbarType.error, ); } } Future deleteComment(String commentId, String contactId) async { try { final success = await ApiService.restoreContactComment(commentId, false); if (success) { if (editingCommentId.value == commentId) editingCommentId.value = null; await fetchCommentsForContact(contactId, active: true); await fetchCommentsForContact(contactId, active: false); showAppSnackbar( title: "Deleted", message: "Comment deleted successfully.", type: SnackbarType.success, ); } else { showAppSnackbar( title: "Error", message: "Failed to delete comment.", type: SnackbarType.error, ); } } catch (e, stack) { logSafe("Delete comment failed: $e", level: LogLevel.error); logSafe(stack.toString(), level: LogLevel.debug); showAppSnackbar( title: "Error", message: "Something went wrong while deleting comment.", type: SnackbarType.error, ); } } Future restoreComment(String commentId, String contactId) async { try { final success = await ApiService.restoreContactComment(commentId, true); if (success) { await fetchCommentsForContact(contactId, active: true); await fetchCommentsForContact(contactId, active: false); showAppSnackbar( title: "Restored", message: "Comment restored successfully.", type: SnackbarType.success, ); } else { showAppSnackbar( title: "Error", message: "Failed to restore comment.", type: SnackbarType.error, ); } } catch (e, stack) { logSafe("Restore comment failed: $e", level: LogLevel.error); logSafe(stack.toString(), level: LogLevel.debug); showAppSnackbar( title: "Error", message: "Something went wrong while restoring comment.", type: SnackbarType.error, ); } } // -------------------- CONTACTS HANDLING -------------------- 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); } } // -------------------- CONTACT DELETION / RESTORE -------------------- Future deleteContact(String contactId) async { try { final success = await ApiService.deleteDirectoryContact(contactId); if (success) { // Refresh contacts after deletion await fetchContacts(active: true); await fetchContacts(active: false); showAppSnackbar( title: "Deleted", message: "Contact deleted successfully.", type: SnackbarType.success, ); } else { showAppSnackbar( title: "Error", message: "Failed to delete contact.", type: SnackbarType.error, ); } } catch (e, stack) { logSafe("Delete contact failed: $e", level: LogLevel.error); logSafe(stack.toString(), level: LogLevel.debug); showAppSnackbar( title: "Error", message: "Something went wrong while deleting contact.", type: SnackbarType.error, ); } } Future restoreContact(String contactId) async { try { final success = await ApiService.restoreDirectoryContact(contactId); if (success) { // Refresh contacts after restore await fetchContacts(active: true); await fetchContacts(active: false); showAppSnackbar( title: "Restored", message: "Contact restored successfully.", type: SnackbarType.success, ); } else { showAppSnackbar( title: "Error", message: "Failed to restore contact.", type: SnackbarType.error, ); } } catch (e, stack) { logSafe("Restore contact failed: $e", level: LogLevel.error); logSafe(stack.toString(), level: LogLevel.debug); showAppSnackbar( title: "Error", message: "Something went wrong while restoring contact.", type: SnackbarType.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.putIfAbsent(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(); 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; } }