diff --git a/lib/helpers/widgets/team_members_bottom_sheet.dart b/lib/helpers/widgets/team_members_bottom_sheet.dart index b73536c..be09523 100644 --- a/lib/helpers/widgets/team_members_bottom_sheet.dart +++ b/lib/helpers/widgets/team_members_bottom_sheet.dart @@ -1,9 +1,16 @@ import 'package:flutter/material.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/avatar.dart'; +import 'package:marco/model/directory/contact_bucket_list_model.dart'; class TeamMembersBottomSheet { - static void show(BuildContext context, List members) { + static void show( + BuildContext context, + ContactBucket bucket, + List members, { + bool canEdit = false, + VoidCallback? onEdit, + }) { showModalBottomSheet( context: context, isScrollControlled: true, @@ -19,14 +26,13 @@ class TeamMembersBottomSheet { ), child: DraggableScrollableSheet( expand: false, - initialChildSize: 0.6, - minChildSize: 0.4, - maxChildSize: 0.9, + initialChildSize: 0.7, + minChildSize: 0.5, + maxChildSize: 0.95, builder: (context, scrollController) { return Column( children: [ const SizedBox(height: 6), - // Drag handle Container( width: 36, height: 4, @@ -36,18 +42,170 @@ class TeamMembersBottomSheet { ), ), const SizedBox(height: 10), + // Title at top center + MyText.titleMedium( + 'Bucket Details', + fontWeight: 700, + ), + + const SizedBox(height: 12), + + // Header with title and optional edit button + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded( + child: MyText.titleMedium( + bucket.name, + fontWeight: 700, + ), + ), + if (canEdit) + IconButton( + onPressed: onEdit, + icon: const Icon(Icons.edit, color: Colors.red), + tooltip: 'Edit Bucket', + ), + ], + ), + ), + + // Bucket info Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - MyText.titleMedium('Team Members', fontWeight: 700), - const SizedBox(height: 6), + if (bucket.description.isNotEmpty) + Padding( + padding: const EdgeInsets.only(bottom: 6), + child: MyText.bodySmall( + bucket.description, + color: Colors.grey[700], + ), + ), + Row( + children: [ + const Icon(Icons.contacts_outlined, + size: 14, color: Colors.grey), + const SizedBox(width: 4), + MyText.labelSmall( + '${bucket.numberOfContacts} contact(s)', + fontWeight: 600, + color: Colors.red, + ), + const SizedBox(width: 12), + const Icon(Icons.ios_share_outlined, + size: 14, color: Colors.grey), + const SizedBox(width: 4), + MyText.labelSmall( + 'Shared with (${members.length})', + fontWeight: 600, + color: Colors.indigo, + ), + ], + ), + + // Can edit indicator + Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + children: [ + const Icon(Icons.edit_outlined, + size: 14, color: Colors.grey), + const SizedBox(width: 4), + MyText.labelSmall( + canEdit + ? 'Can be edited by you' + : 'You don’t have edit access', + fontWeight: 600, + color: canEdit ? Colors.green : Colors.grey, + ), + ], + ), + ), + + const SizedBox(height: 10), + + // Created by + Row( + children: [ + CircleAvatar( + radius: 14, + backgroundColor: Colors.grey.shade300, + backgroundImage: + bucket.createdBy.photo != null && + bucket.createdBy.photo!.isNotEmpty + ? NetworkImage(bucket.createdBy.photo!) + : null, + child: bucket.createdBy.photo == null + ? Text( + bucket.createdBy.firstName.isNotEmpty + ? bucket.createdBy.firstName[0] + : '?', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ) + : null, + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: MyText.labelSmall( + '${bucket.createdBy.firstName} ${bucket.createdBy.lastName}', + fontWeight: 600, + ), + ), + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: + BorderRadius.circular(4), + ), + child: MyText.labelSmall( + "Owner", + fontWeight: 600, + color: Colors.red, + ), + ), + ], + ), + MyText.bodySmall( + bucket.createdBy.jobRoleName, + color: Colors.grey[600], + ), + ], + ), + ), + ], + ), + + const SizedBox(height: 12), const Divider(thickness: 1), + const SizedBox(height: 6), + MyText.labelLarge( + 'Shared with', + fontWeight: 700, + color: Colors.black, + ), ], ), ), + const SizedBox(height: 4), + + // Members list Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -63,7 +221,7 @@ class TeamMembersBottomSheet { controller: scrollController, itemCount: members.length, separatorBuilder: (_, __) => - const SizedBox(height: 4), // tighter spacing + const SizedBox(height: 4), itemBuilder: (context, index) { final member = members[index]; final firstName = member.firstName ?? ''; @@ -75,7 +233,7 @@ class TeamMembersBottomSheet { leading: Avatar( firstName: firstName, lastName: lastName, - size: 32, // smaller avatar + size: 32, ), title: MyText.bodyMedium( '${firstName.isNotEmpty ? firstName : 'Unnamed'} ${lastName.isNotEmpty ? lastName : ''}', diff --git a/lib/view/directory/directory_view.dart b/lib/view/directory/directory_view.dart index aca2692..5e1bcd3 100644 --- a/lib/view/directory/directory_view.dart +++ b/lib/view/directory/directory_view.dart @@ -15,7 +15,6 @@ 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 StatefulWidget { @override @@ -268,45 +267,41 @@ class _DirectoryViewState extends State { itemBuilder: (context) { List> menuItems = []; - // Section: Actions - if (permissionController - .hasPermission(Permissions.directoryAdmin) || - permissionController - .hasPermission(Permissions.directoryManager)) { - menuItems.add( - const PopupMenuItem( - enabled: false, - height: 30, - child: Text( - "Actions", - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.grey), + // Section: Actions (Always visible now) + menuItems.add( + const PopupMenuItem( + enabled: false, + height: 30, + child: Text( + "Actions", + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.grey, ), ), - ); + ), + ); - menuItems.add( - PopupMenuItem( - value: 1, - child: Row( - children: const [ - Icon(Icons.label_outline, - size: 20, color: Colors.black87), - SizedBox(width: 10), - Expanded(child: Text("Manage Buckets")), - Icon(Icons.chevron_right, - size: 20, color: Colors.red), - ], - ), - onTap: () { - Future.delayed(Duration.zero, () { - _handleManageBuckets(); - }); - }, + menuItems.add( + PopupMenuItem( + value: 1, + child: Row( + children: const [ + Icon(Icons.label_outline, + size: 20, color: Colors.black87), + SizedBox(width: 10), + Expanded(child: Text("Manage Buckets")), + Icon(Icons.chevron_right, + size: 20, color: Colors.red), + ], ), - ); - } + onTap: () { + Future.delayed(Duration.zero, () { + _handleManageBuckets(); + }); + }, + ), + ); // Section: Preferences menuItems.add( @@ -316,8 +311,9 @@ class _DirectoryViewState extends State { child: Text( "Preferences", style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.grey), + fontWeight: FontWeight.bold, + color: Colors.grey, + ), ), ), ); diff --git a/lib/view/directory/manage_bucket_screen.dart b/lib/view/directory/manage_bucket_screen.dart index 31745ea..c12ae90 100644 --- a/lib/view/directory/manage_bucket_screen.dart +++ b/lib/view/directory/manage_bucket_screen.dart @@ -109,8 +109,8 @@ class _ManageBucketsScreenState extends State { decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0), - prefixIcon: const Icon(Icons.search, - size: 18, color: Colors.grey), + prefixIcon: + const Icon(Icons.search, size: 18, color: Colors.grey), suffixIcon: searchText.isNotEmpty ? IconButton( icon: const Icon(Icons.close, color: Colors.grey), @@ -134,12 +134,11 @@ class _ManageBucketsScreenState extends State { ), Expanded( child: Obx(() { - final buckets = directoryController.contactBuckets.where((bucket) { + final buckets = + directoryController.contactBuckets.where((bucket) { return bucket.name.toLowerCase().contains(searchText) || bucket.description.toLowerCase().contains(searchText) || - bucket.numberOfContacts - .toString() - .contains(searchText); + bucket.numberOfContacts.toString().contains(searchText); }).toList(); if (directoryController.isLoading.value || @@ -152,7 +151,8 @@ class _ManageBucketsScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.folder_off, size: 48, color: Colors.grey), + const Icon(Icons.folder_off, + size: 48, color: Colors.grey), MySpacing.height(12), MyText.bodyMedium("No buckets available.", fontWeight: 600, color: Colors.grey[700]), @@ -176,147 +176,128 @@ class _ManageBucketsScreenState extends State { .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, + final matchedEmployees = manageBucketController.allEmployees + .where((emp) => bucket.employeeIds.contains(emp.id)) + .toList(); + + return GestureDetector( + onTap: () { + TeamMembersBottomSheet.show( + context, + bucket, + matchedEmployees, + canEdit: canEdit, + onEdit: () { + print('Edit bucket: ${bucket.name}'); + }, + ); + }, + child: 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: [ + MyText.titleSmall( + bucket.name, + fontWeight: 700, + overflow: TextOverflow.ellipsis, + ), + 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, ), - ), - 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( + MySpacing.width(12), + Row( children: [ const Icon(Icons.ios_share_outlined, size: 14, color: Colors.grey), MySpacing.width(4), MyText.labelSmall( - 'Shared with', + 'Shared with (${matchedEmployees.length})', 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, + ), ), ), - ), - ], - ); - }, + ], + ); + }, + ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ); }, );