feat(bucket): add search functionality to Manage Buckets screen

This commit is contained in:
Vaibhav Surve 2025-07-15 10:52:36 +05:30
parent f9ab336eb0
commit 9c28dc05dd

View File

@ -23,7 +23,17 @@ class _ManageBucketsScreenState extends State<ManageBucketsScreen> {
final ManageBucketController manageBucketController =
Get.put(ManageBucketController());
final ProjectController projectController = Get.find();
final Map<String, bool> _expandedMap = {};
final TextEditingController searchController = TextEditingController();
String searchText = '';
void _clearSearch() {
searchController.clear();
setState(() {
searchText = '';
});
}
@override
Widget build(BuildContext context) {
@ -55,28 +65,26 @@ class _ManageBucketsScreenState extends State<ManageBucketsScreen> {
MyText.titleLarge('Manage Buckets',
fontWeight: 700, color: Colors.black),
MySpacing.height(2),
GetBuilder<ProjectController>(
builder: (_) {
final projectName =
projectController.selectedProject?.name ??
'Select Project';
return Row(
children: [
const Icon(Icons.work_outline,
size: 14, color: Colors.grey),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
projectName,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
),
GetBuilder<ProjectController>(builder: (_) {
final projectName =
projectController.selectedProject?.name ??
'Select Project';
return Row(
children: [
const Icon(Icons.work_outline,
size: 14, color: Colors.grey),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
projectName,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
),
],
);
},
),
),
],
);
}),
],
),
),
@ -85,184 +93,237 @@ class _ManageBucketsScreenState extends State<ManageBucketsScreen> {
),
),
),
body: Obx(() {
final buckets = directoryController.contactBuckets;
if (directoryController.isLoading.value ||
manageBucketController.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (buckets.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.folder_off, size: 48, color: Colors.grey),
MySpacing.height(12),
MyText.bodyMedium("No buckets available.",
fontWeight: 600, color: Colors.grey[700]),
],
),
);
}
return ListView.separated(
padding: MySpacing.fromLTRB(16, 20, 16, 24),
itemCount: buckets.length,
separatorBuilder: (_, __) => MySpacing.height(16),
itemBuilder: (context, index) {
final bucket = buckets[index];
final isOwner =
currentUserId != null && bucket.createdBy.id == currentUserId;
final canEdit = isOwner ||
widget.permissionController
.hasPermission(Permissions.directoryAdmin) ||
widget.permissionController
.hasPermission(Permissions.directoryManager);
final isExpanded = _expandedMap[bucket.id] ?? false;
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(top: 4),
child:
Icon(Icons.label_outline, size: 26, color: Colors.indigo),
body: Column(
children: [
Padding(
padding: MySpacing.xy(16, 12),
child: SizedBox(
height: 38,
child: TextField(
controller: searchController,
onChanged: (value) {
setState(() {
searchText = value.toLowerCase();
});
},
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 0),
prefixIcon: const Icon(Icons.search,
size: 18, color: Colors.grey),
suffixIcon: searchText.isNotEmpty
? IconButton(
icon: const Icon(Icons.close, color: Colors.grey),
onPressed: _clearSearch,
)
: null,
hintText: 'Search buckets...',
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
),
MySpacing.width(12),
Expanded(
),
),
),
Expanded(
child: Obx(() {
final buckets = directoryController.contactBuckets.where((bucket) {
return bucket.name.toLowerCase().contains(searchText) ||
bucket.description.toLowerCase().contains(searchText) ||
bucket.numberOfContacts
.toString()
.contains(searchText);
}).toList();
if (directoryController.isLoading.value ||
manageBucketController.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (buckets.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.folder_off, size: 48, color: Colors.grey),
MySpacing.height(12),
MyText.bodyMedium("No buckets available.",
fontWeight: 600, color: Colors.grey[700]),
],
),
);
}
return ListView.separated(
padding: MySpacing.fromLTRB(16, 0, 16, 24),
itemCount: buckets.length,
separatorBuilder: (_, __) => MySpacing.height(16),
itemBuilder: (context, index) {
final bucket = buckets[index];
final isOwner = currentUserId != null &&
bucket.createdBy.id == currentUserId;
final canEdit = isOwner ||
widget.permissionController
.hasPermission(Permissions.directoryAdmin) ||
widget.permissionController
.hasPermission(Permissions.directoryManager);
final isExpanded = _expandedMap[bucket.id] ?? false;
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: MyText.titleSmall(
bucket.name,
fontWeight: 700,
overflow: TextOverflow.ellipsis,
),
),
if (canEdit)
IconButton(
icon: const Icon(Icons.edit_outlined,
size: 20, color: Colors.red),
tooltip: 'View Members',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
final matchedEmployees = manageBucketController
.allEmployees
.where((emp) =>
bucket.employeeIds.contains(emp.id))
.toList();
TeamMembersBottomSheet.show(
context, matchedEmployees);
},
),
],
const Padding(
padding: EdgeInsets.only(top: 4),
child: Icon(Icons.label_outline,
size: 26, color: Colors.indigo),
),
MySpacing.height(4),
Row(
children: [
const Icon(Icons.contacts_outlined,
size: 14, color: Colors.grey),
MySpacing.width(4),
MyText.labelSmall(
'${bucket.numberOfContacts} contact(s)',
color: Colors.red,
fontWeight: 600,
),
MySpacing.width(12),
GestureDetector(
onTap: () {
final matchedEmployees = manageBucketController
.allEmployees
.where((emp) =>
bucket.employeeIds.contains(emp.id))
.toList();
TeamMembersBottomSheet.show(
context, matchedEmployees);
},
child: Row(
MySpacing.width(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.ios_share_outlined,
Expanded(
child: MyText.titleSmall(
bucket.name,
fontWeight: 700,
overflow: TextOverflow.ellipsis,
),
),
if (canEdit)
IconButton(
icon: const Icon(Icons.edit_outlined,
size: 20, color: Colors.red),
tooltip: 'View Members',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
final matchedEmployees =
manageBucketController.allEmployees
.where((emp) =>
bucket.employeeIds
.contains(emp.id))
.toList();
TeamMembersBottomSheet.show(
context, matchedEmployees);
},
),
],
),
MySpacing.height(4),
Row(
children: [
const Icon(Icons.contacts_outlined,
size: 14, color: Colors.grey),
MySpacing.width(4),
MyText.labelSmall(
'Shared with',
color: Colors.indigo,
'${bucket.numberOfContacts} contact(s)',
color: Colors.red,
fontWeight: 600,
),
MySpacing.width(12),
GestureDetector(
onTap: () {
final matchedEmployees =
manageBucketController.allEmployees
.where((emp) =>
bucket.employeeIds
.contains(emp.id))
.toList();
TeamMembersBottomSheet.show(
context, matchedEmployees);
},
child: Row(
children: [
const Icon(Icons.ios_share_outlined,
size: 14, color: Colors.grey),
MySpacing.width(4),
MyText.labelSmall(
'Shared with',
color: Colors.indigo,
fontWeight: 600,
),
],
),
),
],
),
),
],
),
if (bucket.description.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4),
child: LayoutBuilder(
builder: (context, constraints) {
final span = TextSpan(
text: bucket.description,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: Colors.grey[700]),
);
final tp = TextPainter(
text: span,
maxLines: 2,
textDirection: TextDirection.ltr,
)..layout(maxWidth: constraints.maxWidth);
if (bucket.description.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4),
child: LayoutBuilder(
builder: (context, constraints) {
final span = TextSpan(
text: bucket.description,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: Colors.grey[700]),
);
final tp = TextPainter(
text: span,
maxLines: 2,
textDirection: TextDirection.ltr,
)..layout(maxWidth: constraints.maxWidth);
final hasOverflow = tp.didExceedMaxLines;
final hasOverflow = tp.didExceedMaxLines;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodySmall(
bucket.description,
color: Colors.grey[700],
maxLines: isExpanded ? null : 2,
overflow: isExpanded
? TextOverflow.visible
: TextOverflow.ellipsis,
),
if (hasOverflow)
GestureDetector(
onTap: () {
setState(() {
_expandedMap[bucket.id] = !isExpanded;
});
},
child: Padding(
padding: const EdgeInsets.only(top: 4),
child: MyText.labelSmall(
isExpanded
? "Show less"
: "Show more",
fontWeight: 600,
color: Colors.red,
return Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
MyText.bodySmall(
bucket.description,
color: Colors.grey[700],
maxLines: isExpanded ? null : 2,
overflow: isExpanded
? TextOverflow.visible
: TextOverflow.ellipsis,
),
),
),
],
);
},
),
if (hasOverflow)
GestureDetector(
onTap: () {
setState(() {
_expandedMap[bucket.id] =
!isExpanded;
});
},
child: Padding(
padding: const EdgeInsets.only(
top: 4),
child: MyText.labelSmall(
isExpanded
? "Show less"
: "Show more",
fontWeight: 600,
color: Colors.red,
),
),
),
],
);
},
),
),
],
),
),
],
),
),
],
);
},
);
}),
);
},
);
}),
),
],
),
);
}
}