feat(team_members): enhance Team Members Bottom Sheet with bucket details and edit functionality
This commit is contained in:
parent
219815dd27
commit
e624fb00a0
@ -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<dynamic> members) {
|
||||
static void show(
|
||||
BuildContext context,
|
||||
ContactBucket bucket,
|
||||
List<dynamic> 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 : ''}',
|
||||
|
@ -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<DirectoryView> {
|
||||
itemBuilder: (context) {
|
||||
List<PopupMenuEntry<int>> menuItems = [];
|
||||
|
||||
// Section: Actions
|
||||
if (permissionController
|
||||
.hasPermission(Permissions.directoryAdmin) ||
|
||||
permissionController
|
||||
.hasPermission(Permissions.directoryManager)) {
|
||||
menuItems.add(
|
||||
const PopupMenuItem<int>(
|
||||
enabled: false,
|
||||
height: 30,
|
||||
child: Text(
|
||||
"Actions",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey),
|
||||
// Section: Actions (Always visible now)
|
||||
menuItems.add(
|
||||
const PopupMenuItem<int>(
|
||||
enabled: false,
|
||||
height: 30,
|
||||
child: Text(
|
||||
"Actions",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
|
||||
menuItems.add(
|
||||
PopupMenuItem<int>(
|
||||
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<int>(
|
||||
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<DirectoryView> {
|
||||
child: Text(
|
||||
"Preferences",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey),
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -109,8 +109,8 @@ class _ManageBucketsScreenState extends State<ManageBucketsScreen> {
|
||||
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<ManageBucketsScreen> {
|
||||
),
|
||||
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<ManageBucketsScreen> {
|
||||
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<ManageBucketsScreen> {
|
||||
.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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user