From f9ab336eb0505bcc4a8f4761c64290f63f3ed87c Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 14 Jul 2025 18:56:00 +0530 Subject: [PATCH] feat(directory): implement Manage Buckets screen with employee management functionality --- .../directory/manage_bucket_controller.dart | 36 ++ lib/helpers/utils/permission_constants.dart | 9 +- .../widgets/team_members_bottom_sheet.dart | 103 +++++ lib/view/directory/directory_view.dart | 411 ++++++++++-------- lib/view/directory/edit_bucket_model.dart | 72 +++ lib/view/directory/manage_bucket_screen.dart | 268 ++++++++++++ 6 files changed, 714 insertions(+), 185 deletions(-) create mode 100644 lib/controller/directory/manage_bucket_controller.dart create mode 100644 lib/helpers/widgets/team_members_bottom_sheet.dart create mode 100644 lib/view/directory/edit_bucket_model.dart create mode 100644 lib/view/directory/manage_bucket_screen.dart diff --git a/lib/controller/directory/manage_bucket_controller.dart b/lib/controller/directory/manage_bucket_controller.dart new file mode 100644 index 0000000..7d2678f --- /dev/null +++ b/lib/controller/directory/manage_bucket_controller.dart @@ -0,0 +1,36 @@ +import 'package:get/get.dart'; +import 'package:marco/helpers/services/app_logger.dart'; +import 'package:marco/helpers/services/api_service.dart'; +import 'package:marco/model/employee_model.dart'; + +class ManageBucketController extends GetxController { + RxList allEmployees = [].obs; + RxBool isLoading = false.obs; + + @override + void onInit() { + super.onInit(); + fetchAllEmployees(); + } + + Future fetchAllEmployees() async { + isLoading.value = true; + + try { + final response = await ApiService.getAllEmployees(); + if (response != null && response.isNotEmpty) { + allEmployees.assignAll(response.map((json) => EmployeeModel.fromJson(json))); + logSafe("All Employees fetched for Manage Bucket: ${allEmployees.length}", level: LogLevel.info); + } else { + allEmployees.clear(); + logSafe("No employees found for Manage Bucket.", level: LogLevel.warning); + } + } catch (e) { + allEmployees.clear(); + logSafe("Error fetching employees in Manage Bucket", level: LogLevel.error, error: e); + } + + isLoading.value = false; + update(); + } +} diff --git a/lib/helpers/utils/permission_constants.dart b/lib/helpers/utils/permission_constants.dart index 230f161..8160964 100644 --- a/lib/helpers/utils/permission_constants.dart +++ b/lib/helpers/utils/permission_constants.dart @@ -3,9 +3,14 @@ class Permissions { static const String manageProject = "172fc9b6-755b-4f62-ab26-55c34a330614"; static const String viewProjects = "6ea44136-987e-44ba-9e5d-1cf8f5837ebc"; static const String manageEmployees = "a97d366a-c2bb-448d-be93-402bd2324566"; - static const String manageProjectInfra = "f2aee20a-b754-4537-8166-f9507b44585b"; + static const String manageProjectInfra ="f2aee20a-b754-4537-8166-f9507b44585b"; static const String viewProjectInfra = "c7b68e33-72f0-474f-bd96-77636427ecc8"; - static const String regularizeAttendance = "57802c4a-00aa-4a1f-a048-fd2f70dd44b6"; + static const String regularizeAttendance ="57802c4a-00aa-4a1f-a048-fd2f70dd44b6"; static const String assignToProject = "fbd213e0-0250-46f1-9f5f-4b2a1e6e76a3"; static const String infrastructure = "9666de86-d7c7-4d3d-acaa-fcd6d6b81f3c"; + static const String manageTask = "08752f33-3b29-4816-b76b-ea8a968ed3c5"; + static const String viewTask = "9fcc5f87-25e3-4846-90ac-67a71ab92e3c"; + static const String assignReportTask = "6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2"; + static const String directoryAdmin = "4286a13b-bb40-4879-8c6d-18e9e393beda"; + static const String directoryManager = "62668630-13ce-4f52-a0f0-db38af2230c5"; } diff --git a/lib/helpers/widgets/team_members_bottom_sheet.dart b/lib/helpers/widgets/team_members_bottom_sheet.dart new file mode 100644 index 0000000..b73536c --- /dev/null +++ b/lib/helpers/widgets/team_members_bottom_sheet.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/avatar.dart'; + +class TeamMembersBottomSheet { + static void show(BuildContext context, List members) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + isDismissible: true, + enableDrag: true, + builder: (context) { + return SafeArea( + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: DraggableScrollableSheet( + expand: false, + initialChildSize: 0.6, + minChildSize: 0.4, + maxChildSize: 0.9, + builder: (context, scrollController) { + return Column( + children: [ + const SizedBox(height: 6), + // Drag handle + Container( + width: 36, + height: 4, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleMedium('Team Members', fontWeight: 700), + const SizedBox(height: 6), + const Divider(thickness: 1), + ], + ), + ), + const SizedBox(height: 4), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: members.isEmpty + ? Center( + child: MyText.bodySmall( + "No team members found.", + fontWeight: 600, + color: Colors.grey, + ), + ) + : ListView.separated( + controller: scrollController, + itemCount: members.length, + separatorBuilder: (_, __) => + const SizedBox(height: 4), // tighter spacing + itemBuilder: (context, index) { + final member = members[index]; + final firstName = member.firstName ?? ''; + final lastName = member.lastName ?? ''; + + return ListTile( + dense: true, + contentPadding: EdgeInsets.zero, + leading: Avatar( + firstName: firstName, + lastName: lastName, + size: 32, // smaller avatar + ), + title: MyText.bodyMedium( + '${firstName.isNotEmpty ? firstName : 'Unnamed'} ${lastName.isNotEmpty ? lastName : ''}', + fontWeight: 600, + ), + subtitle: MyText.bodySmall( + member.jobRole ?? '', + color: Colors.grey.shade600, + ), + ); + }, + ), + ), + ), + const SizedBox(height: 8), + ], + ); + }, + ), + ), + ); + }, + ); + } +} diff --git a/lib/view/directory/directory_view.dart b/lib/view/directory/directory_view.dart index b2e3807..9c702a5 100644 --- a/lib/view/directory/directory_view.dart +++ b/lib/view/directory/directory_view.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.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'; @@ -10,13 +11,23 @@ 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/view/directory/contact_detail_screen.dart'; -import 'package:marco/controller/directory/create_bucket_controller.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'; -class DirectoryView extends StatelessWidget { +class DirectoryView extends StatefulWidget { + @override + State createState() => _DirectoryViewState(); +} + +class _DirectoryViewState extends State { final DirectoryController controller = Get.find(); final TextEditingController searchController = TextEditingController(); + bool isFabExpanded = false; + final PermissionController permissionController = + Get.put(PermissionController()); Future _refreshDirectory() async { try { @@ -27,116 +38,167 @@ class DirectoryView extends StatelessWidget { } } + 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(16), + ), + 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(12), + ), + 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(12), + ), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + child: const Text("Create Bucket"), + ), + ), + ], + ), + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, - floatingActionButton: FloatingActionButton( - backgroundColor: Colors.red, - onPressed: () async { - await controller.fetchBuckets(); - if (controller.contactBuckets.isEmpty) { - final shouldCreate = await showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (_) { - return SafeArea( - child: Container( - margin: const EdgeInsets.all(16), - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - ), - 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(12), - ), - 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(12), - ), - padding: - const EdgeInsets.symmetric(vertical: 12), - ), - child: const Text("Create Bucket"), - ), - ), - ], - ), - ], - ), + floatingActionButton: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (isFabExpanded) ...[ + Obx(() { + if (permissionController + .hasPermission(Permissions.directoryAdmin) || + permissionController + .hasPermission(Permissions.directoryManager)) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: FloatingActionButton.extended( + heroTag: 'manageBuckets', + backgroundColor: Colors.grey[850], + icon: const Icon(Icons.folder_open_outlined, + color: Colors.white), + label: const Text("Manage Buckets", + style: TextStyle(color: Colors.white)), + onPressed: () { + setState(() => isFabExpanded = false); + _handleManageBuckets(); + }, ), ); - }, - ); - - 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(); - // Proceed to open Add Contact bottom sheet - final result = await Get.bottomSheet( - AddContactBottomSheet(), - isScrollControlled: true, - backgroundColor: Colors.transparent, - ); - - if (result == true) { - controller.fetchContacts(); - } - }, - child: const Icon(Icons.add, color: Colors.white), + } else { + return const SizedBox.shrink(); + } + }), + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: FloatingActionButton.extended( + heroTag: 'createContact', + backgroundColor: Colors.indigo, + icon: const Icon(Icons.person_add_alt_1, color: Colors.white), + label: const Text("Create Contact", + style: TextStyle(color: Colors.white)), + onPressed: () { + setState(() => isFabExpanded = false); + _handleCreateContact(); + }, + ), + ), + ], + FloatingActionButton( + heroTag: 'toggleFab', + backgroundColor: Colors.red, + onPressed: () => setState(() => isFabExpanded = !isFabExpanded), + child: Icon(isFabExpanded ? Icons.close : Icons.add, + color: Colors.white), + ), + ], ), body: Column( children: [ @@ -337,34 +399,62 @@ class DirectoryView extends StatelessWidget { color: Colors.grey[700], overflow: TextOverflow.ellipsis), MySpacing.height(8), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...contact.contactEmails.map((e) => - GestureDetector( - onTap: () => - LauncherUtils.launchEmail( - e.emailAddress), - onLongPress: () => - LauncherUtils.copyToClipboard( - e.emailAddress, - typeLabel: 'Email'), - child: Padding( - padding: const EdgeInsets.only( - bottom: 4), + ...contact.contactEmails.map((e) => + GestureDetector( + onTap: () => LauncherUtils.launchEmail( + e.emailAddress), + onLongPress: () => + LauncherUtils.copyToClipboard( + e.emailAddress, + typeLabel: 'Email'), + child: Padding( + padding: + const EdgeInsets.only(bottom: 4), + child: Row( + children: [ + const Icon(Icons.email_outlined, + size: 16, color: Colors.indigo), + MySpacing.width(4), + ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 180), + child: MyText.labelSmall( + e.emailAddress, + overflow: TextOverflow.ellipsis, + color: Colors.indigo, + decoration: + TextDecoration.underline, + ), + ), + ], + ), + ), + )), + ...contact.contactPhones.map((p) => Padding( + padding: const EdgeInsets.only( + bottom: 8, top: 4), + child: Row( + children: [ + GestureDetector( + onTap: () => + LauncherUtils.launchPhone( + p.phoneNumber), + onLongPress: () => + LauncherUtils.copyToClipboard( + p.phoneNumber, + typeLabel: 'Phone'), child: Row( - mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.email_outlined, + const Icon(Icons.phone_outlined, size: 16, color: Colors.indigo), MySpacing.width(4), ConstrainedBox( constraints: const BoxConstraints( - maxWidth: 180), + maxWidth: 140), child: MyText.labelSmall( - e.emailAddress, + p.phoneNumber, overflow: TextOverflow.ellipsis, color: Colors.indigo, @@ -375,63 +465,19 @@ class DirectoryView extends StatelessWidget { ], ), ), - )), - ...contact.contactPhones.map((p) => Padding( - padding: const EdgeInsets.only( - bottom: 8, top: 4), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - GestureDetector( - onTap: () => - LauncherUtils.launchPhone( - p.phoneNumber), - onLongPress: () => LauncherUtils - .copyToClipboard( - p.phoneNumber, - typeLabel: - 'Phone number'), - child: Row( - mainAxisSize: - MainAxisSize.min, - children: [ - const Icon( - Icons.phone_outlined, - size: 16, - color: Colors.indigo), - MySpacing.width(4), - ConstrainedBox( - constraints: - const BoxConstraints( - maxWidth: 140), - child: MyText.labelSmall( - p.phoneNumber, - overflow: TextOverflow - .ellipsis, - color: Colors.indigo, - decoration: - TextDecoration - .underline, - ), - ), - ], - ), - ), - MySpacing.width(8), - GestureDetector( - onTap: () => LauncherUtils - .launchWhatsApp( - p.phoneNumber), - child: const FaIcon( - FontAwesomeIcons.whatsapp, - color: Colors.green, - size: 16), - ), - ], + MySpacing.width(8), + GestureDetector( + onTap: () => + LauncherUtils.launchWhatsApp( + p.phoneNumber), + child: const FaIcon( + FontAwesomeIcons.whatsapp, + color: Colors.green, + size: 16), ), - )) - ], - ), + ], + ), + )), if (tags.isNotEmpty) ...[ MySpacing.height(2), MyText.labelSmall(tags.join(', '), @@ -443,7 +489,6 @@ class DirectoryView extends StatelessWidget { ), ), Column( - mainAxisAlignment: MainAxisAlignment.start, children: [ const Icon(Icons.arrow_forward_ios, color: Colors.grey, size: 16), diff --git a/lib/view/directory/edit_bucket_model.dart b/lib/view/directory/edit_bucket_model.dart new file mode 100644 index 0000000..b4a331f --- /dev/null +++ b/lib/view/directory/edit_bucket_model.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; + +class EditBucketBottomSheet extends StatelessWidget { + const EditBucketBottomSheet({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + padding: MySpacing.xy(20, 16), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Wrap( + children: [ + Center( + child: MyText.titleMedium( + "Edit Bucket", + fontWeight: 700, + ), + ), + MySpacing.height(16), + MyText.bodyMedium("Bucket Name", fontWeight: 600), + MySpacing.height(6), + const TextField( + decoration: InputDecoration( + hintText: 'Sample Bucket Name', + border: OutlineInputBorder(), + ), + ), + MySpacing.height(16), + MyText.bodyMedium("Description", fontWeight: 600), + MySpacing.height(6), + const TextField( + maxLines: 3, + decoration: InputDecoration( + hintText: 'Sample description...', + border: OutlineInputBorder(), + ), + ), + MySpacing.height(24), + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.indigo, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () { + print("Save clicked (static)"); + Navigator.pop(context); // Dismiss the sheet + }, + child: const Text( + "Save", + style: + TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/view/directory/manage_bucket_screen.dart b/lib/view/directory/manage_bucket_screen.dart new file mode 100644 index 0000000..e38fc8f --- /dev/null +++ b/lib/view/directory/manage_bucket_screen.dart @@ -0,0 +1,268 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/directory/directory_controller.dart'; +import 'package:marco/controller/directory/manage_bucket_controller.dart'; +import 'package:marco/controller/permission_controller.dart'; +import 'package:marco/controller/project_controller.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/utils/permission_constants.dart'; +import 'package:marco/helpers/widgets/team_members_bottom_sheet.dart'; + +class ManageBucketsScreen extends StatefulWidget { + final PermissionController permissionController; + + const ManageBucketsScreen({super.key, required this.permissionController}); + + @override + State createState() => _ManageBucketsScreenState(); +} + +class _ManageBucketsScreenState extends State { + final DirectoryController directoryController = Get.find(); + final ManageBucketController manageBucketController = + Get.put(ManageBucketController()); + final ProjectController projectController = Get.find(); + final Map _expandedMap = {}; + + @override + Widget build(BuildContext context) { + final currentUserId = widget.permissionController.employeeInfo.value?.id; + + return Scaffold( + backgroundColor: Colors.white, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(72), + child: AppBar( + backgroundColor: const Color(0xFFF5F5F5), + elevation: 0.5, + automaticallyImplyLeading: false, + titleSpacing: 0, + title: Padding( + padding: MySpacing.xy(16, 0), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios_new, + color: Colors.black, size: 20), + onPressed: () => Get.back(), + ), + MySpacing.width(8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleLarge('Manage Buckets', + fontWeight: 700, color: Colors.black), + MySpacing.height(2), + GetBuilder( + 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], + ), + ), + ], + ); + }, + ), + ], + ), + ), + ], + ), + ), + ), + ), + 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), + ), + MySpacing.width(12), + Expanded( + child: Column( + 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); + }, + ), + ], + ), + 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( + 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); + + 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, + ), + ), + ), + ], + ); + }, + ), + ), + ], + ), + ), + ], + ); + }, + ); + }), + ); + } +}