import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; import 'package:marco/controller/directory/directory_controller.dart'; import 'package:marco/controller/directory/create_bucket_controller.dart'; import 'package:marco/helpers/utils/launcher_utils.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; import 'package:marco/model/directory/add_contact_bottom_sheet.dart'; import 'package:marco/model/directory/directory_filter_bottom_sheet.dart'; import 'package:marco/model/directory/create_bucket_bottom_sheet.dart'; import 'package:marco/view/directory/contact_detail_screen.dart'; import 'package:marco/view/directory/manage_bucket_screen.dart'; import 'package:marco/controller/permission_controller.dart'; import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/widgets/my_confirmation_dialog.dart'; class DirectoryView extends StatefulWidget { @override State createState() => _DirectoryViewState(); } class _DirectoryViewState extends State { final DirectoryController controller = Get.find(); final TextEditingController searchController = TextEditingController(); final PermissionController permissionController = Get.put(PermissionController()); Future _refreshDirectory() async { try { await controller.fetchContacts(); } catch (e, stackTrace) { debugPrint('Error refreshing directory data: ${e.toString()}'); debugPrintStack(stackTrace: stackTrace); } } void _handleCreateContact() async { await controller.fetchBuckets(); if (controller.contactBuckets.isEmpty) { final shouldCreate = await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => _buildEmptyBucketPrompt(), ); if (shouldCreate != true) return; final created = await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => const CreateBucketBottomSheet(), ); if (created == true) { await controller.fetchBuckets(); } else { return; } } Get.delete(); final result = await Get.bottomSheet( AddContactBottomSheet(), isScrollControlled: true, backgroundColor: Colors.transparent, ); if (result == true) { controller.fetchContacts(); } } void _handleManageBuckets() async { await controller.fetchBuckets(); Get.to( () => ManageBucketsScreen(permissionController: permissionController)); } Widget _buildEmptyBucketPrompt() { return SafeArea( child: Container( margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(5), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.info_outline, size: 48, color: Colors.indigo), MySpacing.height(12), MyText.titleMedium("No Buckets Assigned", fontWeight: 700, textAlign: TextAlign.center), MySpacing.height(8), MyText.bodyMedium( "You don’t have any buckets assigned. Please create a bucket before adding a contact.", textAlign: TextAlign.center, color: Colors.grey[700], ), MySpacing.height(20), Row( children: [ Expanded( child: ElevatedButton( onPressed: () => Navigator.pop(context, false), style: ElevatedButton.styleFrom( backgroundColor: Colors.grey[300], foregroundColor: Colors.black, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), ), padding: const EdgeInsets.symmetric(vertical: 12), ), child: const Text("Cancel"), ), ), MySpacing.width(12), Expanded( child: ElevatedButton( onPressed: () => Navigator.pop(context, true), style: ElevatedButton.styleFrom( backgroundColor: Colors.indigo, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), ), padding: const EdgeInsets.symmetric(vertical: 12), ), child: const Text("Create Bucket"), ), ), ], ), ], ), ), ); } Widget _buildEmptyState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.perm_contact_cal, size: 60, color: Colors.grey), MySpacing.height(18), MyText.titleMedium( 'No matching contacts found.', fontWeight: 600, color: Colors.grey, ), MySpacing.height(10), MyText.bodySmall( 'Try adjusting your filters or refresh to reload.', color: Colors.grey, ), ], ), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey[100], floatingActionButton: FloatingActionButton.extended( heroTag: 'createContact', backgroundColor: Colors.red, onPressed: _handleCreateContact, icon: const Icon(Icons.person_add_alt_1, color: Colors.white), label: const Text("Add Contact", style: TextStyle(color: Colors.white)), ), body: Column( children: [ // Search + Filter + More menu Padding( padding: MySpacing.xy(8, 8), child: Row( children: [ Expanded( child: SizedBox( height: 35, child: TextField( controller: searchController, onChanged: (value) { controller.searchQuery.value = value; controller.applyFilters(); }, decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 12), prefixIcon: const Icon(Icons.search, size: 20, color: Colors.grey), suffixIcon: ValueListenableBuilder( valueListenable: searchController, builder: (context, value, _) { if (value.text.isEmpty) return const SizedBox.shrink(); return IconButton( icon: const Icon(Icons.clear, size: 20, color: Colors.grey), onPressed: () { searchController.clear(); controller.searchQuery.value = ''; controller.applyFilters(); }, ); }, ), hintText: 'Search contacts...', filled: true, fillColor: Colors.white, border: OutlineInputBorder( borderRadius: BorderRadius.circular(5), borderSide: BorderSide(color: Colors.grey.shade300), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(5), borderSide: BorderSide(color: Colors.grey.shade300), ), ), ), ), ), MySpacing.width(8), Obx(() { final isFilterActive = controller.hasActiveFilters(); return Stack( children: [ Container( height: 35, width: 35, decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(5), ), child: IconButton( icon: Icon(Icons.tune, size: 20, color: isFilterActive ? Colors.indigo : Colors.black87), onPressed: () { showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(5)), ), builder: (_) => const DirectoryFilterBottomSheet(), ); }, ), ), if (isFilterActive) Positioned( top: 6, right: 6, child: Container( height: 8, width: 8, decoration: const BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), ), ), ], ); }), MySpacing.width(10), Container( height: 35, width: 35, decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(5), ), child: PopupMenuButton( padding: EdgeInsets.zero, icon: const Icon(Icons.more_vert, size: 20, color: Colors.black87), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5)), itemBuilder: (context) { List> menuItems = []; // Section: Actions menuItems.add( const PopupMenuItem( enabled: false, height: 30, child: Text("Actions", style: TextStyle( fontWeight: FontWeight.bold, color: Colors.grey)), ), ); if (permissionController .hasPermission(Permissions.directoryAdmin) || permissionController .hasPermission(Permissions.directoryManager) || permissionController .hasPermission(Permissions.directoryUser)) { menuItems.add( PopupMenuItem( value: 2, child: Row( children: const [ Icon(Icons.add_box_outlined, size: 20, color: Colors.black87), SizedBox(width: 10), Expanded(child: Text("Create Bucket")), Icon(Icons.chevron_right, size: 20, color: Colors.red), ], ), onTap: () { Future.delayed(Duration.zero, () async { final created = await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => const CreateBucketBottomSheet(), ); if (created == true) { await controller.fetchBuckets(); } }); }, ), ); } // Manage Buckets option menuItems.add( PopupMenuItem( value: 1, child: Row( children: const [ Icon(Icons.label_outline, size: 20, color: Colors.black87), SizedBox(width: 10), Expanded(child: Text("Manage Buckets")), Icon(Icons.chevron_right, size: 20, color: Colors.red), ], ), onTap: () { Future.delayed(Duration.zero, () { _handleManageBuckets(); }); }, ), ); // Section: Preferences menuItems.add( const PopupMenuItem( enabled: false, height: 30, child: Text("Preferences", style: TextStyle( fontWeight: FontWeight.bold, color: Colors.grey)), ), ); // Show Inactive toggle menuItems.add( PopupMenuItem( value: 0, enabled: false, child: Obx(() => Row( children: [ const Icon(Icons.visibility_off_outlined, size: 20, color: Colors.black87), const SizedBox(width: 10), const Expanded( child: Text('Show Deleted Contacts')), Switch.adaptive( value: !controller.isActive.value, activeColor: Colors.indigo, onChanged: (val) { controller.isActive.value = !val; controller.fetchContacts(active: !val); Navigator.pop(context); }, ), ], )), ), ); return menuItems; }, ), ), ], ), ), // Contact List Expanded( child: Obx(() { return MyRefreshIndicator( onRefresh: _refreshDirectory, backgroundColor: Colors.indigo, color: Colors.white, child: controller.isLoading.value ? ListView.separated( physics: const AlwaysScrollableScrollPhysics(), itemCount: 10, separatorBuilder: (_, __) => MySpacing.height(12), itemBuilder: (_, __) => SkeletonLoaders.contactSkeletonCard(), ) : controller.filteredContacts.isEmpty ? _buildEmptyState() : ListView.separated( physics: const AlwaysScrollableScrollPhysics(), padding: MySpacing.only( left: 8, right: 8, top: 4, bottom: 80), itemCount: controller.filteredContacts.length, separatorBuilder: (_, __) => MySpacing.height(12), itemBuilder: (_, index) { final contact = controller.filteredContacts[index]; final isDeleted = !controller .isActive.value; // mark deleted contacts final nameParts = contact.name.trim().split(" "); final firstName = nameParts.first; final lastName = nameParts.length > 1 ? nameParts.last : ""; final tags = contact.tags .map((tag) => tag.name) .toList(); return Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), ), elevation: 3, shadowColor: Colors.grey.withOpacity(0.3), color: Colors.white, child: InkWell( borderRadius: BorderRadius.circular(5), onTap: isDeleted ? null : () => Get.to(() => ContactDetailScreen( contact: contact)), child: Padding( padding: const EdgeInsets.all(12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Avatar Avatar( firstName: firstName, lastName: lastName, size: 40, backgroundColor: isDeleted ? Colors.grey.shade400 : null, ), MySpacing.width(12), // Contact Info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.titleSmall( contact.name, fontWeight: 600, overflow: TextOverflow.ellipsis, color: isDeleted ? Colors.grey : Colors.black87, ), MyText.bodySmall( contact.organization, color: isDeleted ? Colors.grey : Colors.grey[700], overflow: TextOverflow.ellipsis, ), MySpacing.height(6), if (contact .contactEmails.isNotEmpty) Padding( padding: const EdgeInsets.only( bottom: 4), child: GestureDetector( onTap: isDeleted ? null : () => LauncherUtils .launchEmail(contact .contactEmails .first .emailAddress), onLongPress: isDeleted ? null : () => LauncherUtils .copyToClipboard( contact .contactEmails .first .emailAddress, typeLabel: 'Email', ), child: Row( children: [ Icon( Icons .email_outlined, size: 16, color: isDeleted ? Colors.grey : Colors .indigo), MySpacing.width(4), Expanded( child: MyText .labelSmall( contact .contactEmails .first .emailAddress, overflow: TextOverflow .ellipsis, color: isDeleted ? Colors.grey : Colors .indigo, decoration: TextDecoration .underline, ), ), ], ), ), ), if (contact .contactPhones.isNotEmpty) Padding( padding: const EdgeInsets.only( bottom: 8, top: 4), child: Row( children: [ Expanded( child: GestureDetector( onTap: isDeleted ? null : () => LauncherUtils .launchPhone(contact .contactPhones .first .phoneNumber), onLongPress: isDeleted ? null : () => LauncherUtils .copyToClipboard( contact .contactPhones .first .phoneNumber, typeLabel: 'Phone', ), child: Row( children: [ Icon( Icons .phone_outlined, size: 16, color: isDeleted ? Colors .grey : Colors .indigo), MySpacing.width( 4), Expanded( child: MyText .labelSmall( contact .contactPhones .first .phoneNumber, overflow: TextOverflow .ellipsis, color: isDeleted ? Colors .grey : Colors .indigo, decoration: TextDecoration .underline, ), ), ], ), ), ), MySpacing.width(8), GestureDetector( onTap: isDeleted ? null : () => LauncherUtils .launchWhatsApp(contact .contactPhones .first .phoneNumber), child: FaIcon( FontAwesomeIcons .whatsapp, color: isDeleted ? Colors.grey : Colors .green, size: 25), ), ], ), ), if (tags.isNotEmpty) Padding( padding: const EdgeInsets.only( top: 0), child: Wrap( spacing: 6, runSpacing: 2, children: tags .map( (tag) => Chip( label: Text(tag), backgroundColor: Colors.indigo .shade50, labelStyle: TextStyle( color: isDeleted ? Colors .grey : Colors .indigo, fontSize: 12), visualDensity: VisualDensity .compact, shape: RoundedRectangleBorder( borderRadius: BorderRadius .circular( 5), ), ), ) .toList(), ), ), ], ), ), // Actions Column (Arrow + Icons) Column( children: [ IconButton( icon: Icon( isDeleted ? Icons.restore : Icons.delete, color: isDeleted ? Colors.green : Colors.redAccent, size: 20, ), onPressed: () async { await Get.dialog( ConfirmDialog( title: isDeleted ? "Restore Contact" : "Delete Contact", message: isDeleted ? "Are you sure you want to restore this contact?" : "Are you sure you want to delete this contact?", confirmText: isDeleted ? "Restore" : "Delete", confirmColor: isDeleted ? Colors.green : Colors.redAccent, icon: isDeleted ? Icons.restore : Icons .delete_forever, onConfirm: () async { if (isDeleted) { await controller .restoreContact( contact.id); } else { await controller .deleteContact( contact.id); } }, ), barrierDismissible: false, ); }, ), const SizedBox(height: 4), Icon( Icons.arrow_forward_ios, color: Colors.grey, size: 20, ) ], ), ], ), ), ), ); })); }), ) ], ), ); } }