diff --git a/lib/model/directory/directory_filter_bottom_sheet.dart b/lib/model/directory/directory_filter_bottom_sheet.dart index e39f689..6f10473 100644 --- a/lib/model/directory/directory_filter_bottom_sheet.dart +++ b/lib/model/directory/directory_filter_bottom_sheet.dart @@ -1,170 +1,275 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/controller/directory/directory_controller.dart'; +import 'package:marco/helpers/utils/base_bottom_sheet.dart'; import 'package:marco/helpers/widgets/my_text.dart'; -class DirectoryFilterBottomSheet extends StatelessWidget { +class DirectoryFilterBottomSheet extends StatefulWidget { const DirectoryFilterBottomSheet({super.key}); @override - Widget build(BuildContext context) { - final controller = Get.find(); + State createState() => + _DirectoryFilterBottomSheetState(); +} - return Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom + 20, - top: 12, - left: 16, - right: 16, - ), - child: Obx(() { - return SingleChildScrollView( +class _DirectoryFilterBottomSheetState + extends State { + final DirectoryController controller = Get.find(); + final _categorySearchQuery = ''.obs; + final _bucketSearchQuery = ''.obs; + + final _categoryExpanded = false.obs; + final _bucketExpanded = false.obs; + + late final RxList _tempSelectedCategories; + late final RxList _tempSelectedBuckets; + + @override + void initState() { + super.initState(); + _tempSelectedCategories = controller.selectedCategories.toList().obs; + _tempSelectedBuckets = controller.selectedBuckets.toList().obs; + } + + void _toggleCategory(String id) { + _tempSelectedCategories.contains(id) + ? _tempSelectedCategories.remove(id) + : _tempSelectedCategories.add(id); + } + + void _toggleBucket(String id) { + _tempSelectedBuckets.contains(id) + ? _tempSelectedBuckets.remove(id) + : _tempSelectedBuckets.add(id); + } + + void _resetFilters() { + _tempSelectedCategories.clear(); + _tempSelectedBuckets.clear(); + } + + @override + Widget build(BuildContext context) { + return BaseBottomSheet( + title: "Filter Contacts", + onSubmit: () { + controller.selectedCategories.value = _tempSelectedCategories; + controller.selectedBuckets.value = _tempSelectedBuckets; + controller.applyFilters(); + Get.back(); + }, + onCancel: Get.back, + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), child: Column( + mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - /// Drag handle - Center( - child: Container( - height: 5, - width: 50, - margin: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration( - color: Colors.grey.shade300, - borderRadius: BorderRadius.circular(2.5), - ), - ), - ), - - /// Title - Center( - child: MyText.titleMedium( - "Filter Contacts", - fontWeight: 700, - ), - ), - - const SizedBox(height: 24), - - /// Categories - if (controller.contactCategories.isNotEmpty) ...[ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + Obx(() { + final hasSelections = _tempSelectedCategories.isNotEmpty || + _tempSelectedBuckets.isNotEmpty; + if (!hasSelections) return const SizedBox.shrink(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - MyText.bodyMedium("Categories", fontWeight: 600), + MyText("Selected Filters:", fontWeight: 600), + const SizedBox(height: 4), + _buildChips(_tempSelectedCategories, + controller.contactCategories, _toggleCategory), + _buildChips(_tempSelectedBuckets, controller.contactBuckets, + _toggleBucket), ], - ), - const SizedBox(height: 10), - Wrap( - spacing: 2, - runSpacing: 0, - children: controller.contactCategories.map((category) { - final selected = - controller.selectedCategories.contains(category.id); - return AnimatedContainer( - duration: const Duration(milliseconds: 200), - child: FilterChip( - label: MyText.bodySmall( - category.name, - color: selected ? Colors.white : Colors.black87, - ), - selected: selected, - onSelected: (_) => - controller.toggleCategory(category.id), - selectedColor: Colors.indigo, - backgroundColor: Colors.grey.shade200, - checkmarkColor: Colors.white, - ), - ); - }).toList(), - ), - const SizedBox(height: 12), - ], - - /// Buckets - if (controller.contactBuckets.isNotEmpty) ...[ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - MyText.bodyMedium("Buckets", fontWeight: 600), - ], - ), - const SizedBox(height: 10), - Wrap( - spacing: 2, - runSpacing: 0, - children: controller.contactBuckets.map((bucket) { - final selected = - controller.selectedBuckets.contains(bucket.id); - return AnimatedContainer( - duration: const Duration(milliseconds: 200), - child: FilterChip( - label: MyText.bodySmall( - bucket.name, - color: selected ? Colors.white : Colors.black87, - ), - selected: selected, - onSelected: (_) => controller.toggleBucket(bucket.id), - selectedColor: Colors.teal, - backgroundColor: Colors.grey.shade200, - checkmarkColor: Colors.white, - ), - ); - }).toList(), - ), - ], - - const SizedBox(height: 12), - - /// Action Buttons + ); + }), Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisAlignment: MainAxisAlignment.end, children: [ - OutlinedButton.icon( - onPressed: () { - controller.selectedCategories.clear(); - controller.selectedBuckets.clear(); - controller.searchQuery.value = ''; - controller.applyFilters(); - Get.back(); - }, - icon: const Icon(Icons.refresh, color: Colors.red), - label: MyText.bodyMedium("Clear", color: Colors.red), - style: OutlinedButton.styleFrom( - side: const BorderSide(color: Colors.red), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 7), - ), - ), - ElevatedButton.icon( - onPressed: () { - controller.applyFilters(); - Get.back(); - }, - icon: const Icon(Icons.check_circle_outline), - label: MyText.bodyMedium("Apply", color: Colors.white), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.indigo, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 7), + TextButton.icon( + onPressed: _resetFilters, + icon: const Icon(Icons.restart_alt, size: 18), + label: MyText("Reset All", color: Colors.red), + style: TextButton.styleFrom( + foregroundColor: Colors.red.shade400, ), ), ], ), - const SizedBox(height: 10), + if (controller.contactCategories.isNotEmpty) + Obx(() => _buildExpandableFilterSection( + title: "Categories", + expanded: _categoryExpanded, + searchQuery: _categorySearchQuery, + allItems: controller.contactCategories, + selectedItems: _tempSelectedCategories, + onToggle: _toggleCategory, + )), + if (controller.contactBuckets.isNotEmpty) + Obx(() => _buildExpandableFilterSection( + title: "Buckets", + expanded: _bucketExpanded, + searchQuery: _bucketSearchQuery, + allItems: controller.contactBuckets, + selectedItems: _tempSelectedBuckets, + onToggle: _toggleBucket, + )), ], ), - ); - }), + ), + ), + ); + } + + Widget _buildChips(RxList selectedIds, List allItems, + Function(String) onRemoved) { + final idToName = {for (var item in allItems) item.id: item.name}; + return Wrap( + spacing: 4, + runSpacing: 4, + children: selectedIds + .map((id) => Chip( + label: MyText(idToName[id] ?? "", color: Colors.black87), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => onRemoved(id), + backgroundColor: Colors.blue.shade50, + )) + .toList(), + ); + } + + Widget _buildExpandableFilterSection({ + required String title, + required RxBool expanded, + required RxString searchQuery, + required List allItems, + required RxList selectedItems, + required Function(String) onToggle, + }) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Column( + children: [ + GestureDetector( + onTap: () => expanded.toggle(), + child: Row( + children: [ + Icon( + expanded.value + ? Icons.keyboard_arrow_down + : Icons.keyboard_arrow_right, + size: 20, + ), + const SizedBox(width: 4), + MyText( + "$title (${selectedItems.length})", + fontWeight: 600, + fontSize: 16, + ), + ], + ), + ), + if (expanded.value) + _buildFilterSection( + searchQuery: searchQuery, + allItems: allItems, + selectedItems: selectedItems, + onToggle: onToggle, + title: title, + ), + ], + ), + ); + } + + Widget _buildFilterSection({ + required String title, + required RxString searchQuery, + required List allItems, + required RxList selectedItems, + required Function(String) onToggle, + }) { + final filteredList = allItems.where((item) { + if (searchQuery.isEmpty) return true; + return item.name.toLowerCase().contains(searchQuery.value.toLowerCase()); + }).toList(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 6), + TextField( + onChanged: (value) => searchQuery.value = value, + style: const TextStyle(fontSize: 13), + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + hintText: "Search $title...", + hintStyle: const TextStyle(fontSize: 13), + prefixIcon: const Icon(Icons.search, size: 18), + isDense: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + filled: true, + fillColor: Colors.grey.shade100, + ), + ), + const SizedBox(height: 8), + if (filteredList.isEmpty) + Row( + children: [ + const Icon(Icons.sentiment_dissatisfied, color: Colors.grey), + const SizedBox(width: 10), + MyText("No results found.", + color: Colors.grey.shade600, fontSize: 14), + ], + ) + else + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 230), + child: ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: filteredList.length, + itemBuilder: (context, index) { + final item = filteredList[index]; + final isSelected = selectedItems.contains(item.id); + + return GestureDetector( + onTap: () => onToggle(item.id), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 4, vertical: 6), + child: Row( + children: [ + Container( + width: 18, + height: 18, + decoration: BoxDecoration( + color: + isSelected ? Colors.blueAccent : Colors.white, + border: Border.all( + color: Colors.black, + width: 1.2, + ), + borderRadius: BorderRadius.circular(4), + ), + child: isSelected + ? const Icon(Icons.check, + size: 14, color: Colors.white) + : null, + ), + const SizedBox(width: 8), + MyText(item.name, fontSize: 14), + ], + ), + ), + ); + }, + ), + ) + ], ); } }