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