Vaibhav_Feature-#768 #59
@ -1,170 +1,275 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/controller/directory/directory_controller.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';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
|
|
||||||
class DirectoryFilterBottomSheet extends StatelessWidget {
|
class DirectoryFilterBottomSheet extends StatefulWidget {
|
||||||
const DirectoryFilterBottomSheet({super.key});
|
const DirectoryFilterBottomSheet({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
State<DirectoryFilterBottomSheet> createState() =>
|
||||||
final controller = Get.find<DirectoryController>();
|
_DirectoryFilterBottomSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
return Container(
|
class _DirectoryFilterBottomSheetState
|
||||||
decoration: const BoxDecoration(
|
extends State<DirectoryFilterBottomSheet> {
|
||||||
color: Colors.white,
|
final DirectoryController controller = Get.find<DirectoryController>();
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
final _categorySearchQuery = ''.obs;
|
||||||
),
|
final _bucketSearchQuery = ''.obs;
|
||||||
padding: EdgeInsets.only(
|
|
||||||
bottom: MediaQuery.of(context).viewInsets.bottom + 20,
|
final _categoryExpanded = false.obs;
|
||||||
top: 12,
|
final _bucketExpanded = false.obs;
|
||||||
left: 16,
|
|
||||||
right: 16,
|
late final RxList<String> _tempSelectedCategories;
|
||||||
),
|
late final RxList<String> _tempSelectedBuckets;
|
||||||
child: Obx(() {
|
|
||||||
return SingleChildScrollView(
|
@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(
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
/// Drag handle
|
Obx(() {
|
||||||
Center(
|
final hasSelections = _tempSelectedCategories.isNotEmpty ||
|
||||||
child: Container(
|
_tempSelectedBuckets.isNotEmpty;
|
||||||
height: 5,
|
if (!hasSelections) return const SizedBox.shrink();
|
||||||
width: 50,
|
return Column(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
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,
|
|
||||||
children: [
|
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(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
OutlinedButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: _resetFilters,
|
||||||
controller.selectedCategories.clear();
|
icon: const Icon(Icons.restart_alt, size: 18),
|
||||||
controller.selectedBuckets.clear();
|
label: MyText("Reset All", color: Colors.red),
|
||||||
controller.searchQuery.value = '';
|
style: TextButton.styleFrom(
|
||||||
controller.applyFilters();
|
foregroundColor: Colors.red.shade400,
|
||||||
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),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
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<String> selectedIds, List<dynamic> 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<dynamic> allItems,
|
||||||
|
required RxList<String> 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<dynamic> allItems,
|
||||||
|
required RxList<String> 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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user