feat: Refactor TeamMembersBottomSheet and CreateBucketBottomSheet for improved structure and readability
This commit is contained in:
parent
0f14fda83a
commit
d0cbfa987d
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/helpers/widgets/avatar.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/model/directory/contact_bucket_list_model.dart';
|
||||
|
||||
class TeamMembersBottomSheet {
|
||||
@ -11,8 +12,9 @@ class TeamMembersBottomSheet {
|
||||
bool canEdit = false,
|
||||
VoidCallback? onEdit,
|
||||
}) {
|
||||
// Ensure the owner is at the top of the list
|
||||
final ownerId = bucket.createdBy.id;
|
||||
|
||||
// Ensure owner is first
|
||||
members.sort((a, b) {
|
||||
if (a.id == ownerId) return -1;
|
||||
if (b.id == ownerId) return 1;
|
||||
@ -34,186 +36,24 @@ class TeamMembersBottomSheet {
|
||||
),
|
||||
child: DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: 0.7,
|
||||
minChildSize: 0.5,
|
||||
initialChildSize: 0.75,
|
||||
minChildSize: 0.55,
|
||||
maxChildSize: 0.95,
|
||||
builder: (context, scrollController) {
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
width: 36,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
MyText.titleMedium(
|
||||
'Bucket Details',
|
||||
fontWeight: 700,
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Header with title and edit
|
||||
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',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Info
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
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: 8),
|
||||
const Divider(thickness: 1),
|
||||
const SizedBox(height: 6),
|
||||
MyText.labelLarge(
|
||||
'Shared with',
|
||||
fontWeight: 700,
|
||||
color: Colors.black,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
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),
|
||||
itemBuilder: (context, index) {
|
||||
final member = members[index];
|
||||
final firstName = member.firstName ?? '';
|
||||
final lastName = member.lastName ?? '';
|
||||
final isOwner =
|
||||
member.id == bucket.createdBy.id;
|
||||
|
||||
return ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Avatar(
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
size: 32,
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyText.bodyMedium(
|
||||
'${firstName.isNotEmpty ? firstName : 'Unnamed'} ${lastName.isNotEmpty ? lastName : ''}',
|
||||
fontWeight: 600,
|
||||
),
|
||||
),
|
||||
if (isOwner)
|
||||
Container(
|
||||
margin:
|
||||
const EdgeInsets.only(left: 6),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: MyText.bodySmall(
|
||||
member.jobRole ?? '',
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
MySpacing.height(8),
|
||||
_buildGrabHandle(),
|
||||
MySpacing.height(10),
|
||||
MyText.titleMedium('Bucket Details', fontWeight: 700),
|
||||
MySpacing.height(12),
|
||||
_buildHeader(bucket, canEdit, onEdit),
|
||||
_buildInfo(bucket, members.length, canEdit),
|
||||
MySpacing.height(6),
|
||||
_buildMembersTitle(),
|
||||
MySpacing.height(4),
|
||||
Expanded(child: _buildMemberList(members, ownerId, scrollController)),
|
||||
MySpacing.height(8),
|
||||
],
|
||||
);
|
||||
},
|
||||
@ -223,4 +63,152 @@ class TeamMembersBottomSheet {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildGrabHandle() {
|
||||
return Container(
|
||||
width: 36,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildHeader(ContactBucket bucket, bool canEdit, VoidCallback? onEdit) {
|
||||
return 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',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildInfo(ContactBucket bucket, int totalMembers, bool canEdit) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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 ($totalMembers)',
|
||||
fontWeight: 600,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(8),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(8),
|
||||
const Divider(thickness: 1),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildMembersTitle() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: MyText.labelLarge('Shared with', fontWeight: 700, color: Colors.black),
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildMemberList(List<dynamic> members, String ownerId, ScrollController scrollController) {
|
||||
if (members.isEmpty) {
|
||||
return Center(
|
||||
child: MyText.bodySmall(
|
||||
"No team members found.",
|
||||
fontWeight: 600,
|
||||
color: Colors.grey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
controller: scrollController,
|
||||
itemCount: members.length,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 4),
|
||||
itemBuilder: (context, index) {
|
||||
final member = members[index];
|
||||
final firstName = member.firstName ?? '';
|
||||
final lastName = member.lastName ?? '';
|
||||
final isOwner = member.id == ownerId;
|
||||
|
||||
return ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Avatar(firstName: firstName, lastName: lastName, size: 32),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyText.bodyMedium(
|
||||
'${firstName.isNotEmpty ? firstName : 'Unnamed'} ${lastName.isNotEmpty ? lastName : ''}',
|
||||
fontWeight: 600,
|
||||
),
|
||||
),
|
||||
if (isOwner)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 6),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: MyText.bodySmall(
|
||||
member.jobRole ?? '',
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/controller/directory/create_bucket_controller.dart';
|
||||
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||
@ -38,125 +39,55 @@ class _CreateBucketBottomSheetState extends State<CreateBucketBottomSheet> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _formContent() {
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.labelMedium("Bucket Name"),
|
||||
MySpacing.height(8),
|
||||
TextFormField(
|
||||
initialValue: _controller.name.value,
|
||||
onChanged: _controller.updateName,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return "Bucket name is required";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration: _inputDecoration("e.g., Project Docs"),
|
||||
),
|
||||
MySpacing.height(16),
|
||||
MyText.labelMedium("Description"),
|
||||
MySpacing.height(8),
|
||||
TextFormField(
|
||||
initialValue: _controller.description.value,
|
||||
onChanged: _controller.updateDescription,
|
||||
maxLines: 3,
|
||||
decoration: _inputDecoration("Optional bucket description"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return GetBuilder<BucketController>(
|
||||
builder: (_) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: SingleChildScrollView(
|
||||
padding: MediaQuery.of(context).viewInsets,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.cardColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black12, blurRadius: 12, offset: Offset(0, -2)),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
MySpacing.height(12),
|
||||
Text("Create New Bucket", style: MyTextStyle.titleLarge(fontWeight: 700)),
|
||||
MySpacing.height(24),
|
||||
Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.labelMedium("Bucket Name"),
|
||||
MySpacing.height(8),
|
||||
TextFormField(
|
||||
initialValue: _controller.name.value,
|
||||
onChanged: _controller.updateName,
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return "Bucket name is required";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration: _inputDecoration("e.g., Project Docs"),
|
||||
),
|
||||
MySpacing.height(16),
|
||||
MyText.labelMedium("Description"),
|
||||
MySpacing.height(8),
|
||||
TextFormField(
|
||||
initialValue: _controller.description.value,
|
||||
onChanged: _controller.updateDescription,
|
||||
maxLines: 3,
|
||||
decoration: _inputDecoration("Optional bucket description"),
|
||||
),
|
||||
MySpacing.height(24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
icon: const Icon(Icons.close, color: Colors.red),
|
||||
label: MyText.bodyMedium("Cancel", color: Colors.red, fontWeight: 600),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: const BorderSide(color: Colors.red),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
return ElevatedButton.icon(
|
||||
onPressed: _controller.isCreating.value
|
||||
? null
|
||||
: () async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
await _controller.createBucket();
|
||||
}
|
||||
},
|
||||
icon: _controller.isCreating.value
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.check_circle_outline, color: Colors.white),
|
||||
label: MyText.bodyMedium(
|
||||
_controller.isCreating.value ? "Creating..." : "Create",
|
||||
color: Colors.white,
|
||||
fontWeight: 600,
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.indigo,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 14),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: BaseBottomSheet(
|
||||
title: "Create New Bucket",
|
||||
child: _formContent(),
|
||||
onCancel: () => Navigator.pop(context, false),
|
||||
onSubmit: () async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
await _controller.createBucket();
|
||||
}
|
||||
},
|
||||
isSubmitting: _controller.isCreating.value,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -161,7 +161,7 @@ class _DirectoryFilterBottomSheetState
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
MyText(
|
||||
"$title (${selectedItems.length})",
|
||||
"$title",
|
||||
fontWeight: 600,
|
||||
fontSize: 16,
|
||||
),
|
||||
|
@ -1,18 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:marco/controller/directory/manage_bucket_controller.dart';
|
||||
import 'package:marco/controller/directory/directory_controller.dart';
|
||||
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/model/directory/contact_bucket_list_model.dart';
|
||||
import 'package:marco/model/employee_model.dart';
|
||||
import 'package:marco/controller/directory/directory_controller.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
import 'package:marco/model/employee_model.dart';
|
||||
import 'package:marco/model/directory/contact_bucket_list_model.dart';
|
||||
|
||||
class EditBucketBottomSheet {
|
||||
static void show(BuildContext context, ContactBucket bucket,
|
||||
List<EmployeeModel> allEmployees,
|
||||
{required String ownerId}) {
|
||||
static void show(
|
||||
BuildContext context,
|
||||
ContactBucket bucket,
|
||||
List<EmployeeModel> allEmployees, {
|
||||
required String ownerId,
|
||||
}) {
|
||||
final ManageBucketController controller = Get.find();
|
||||
|
||||
final nameController = TextEditingController(text: bucket.name);
|
||||
@ -25,7 +29,6 @@ class EditBucketBottomSheet {
|
||||
InputDecoration _inputDecoration(String label) {
|
||||
return InputDecoration(
|
||||
labelText: label,
|
||||
hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade100,
|
||||
border: OutlineInputBorder(
|
||||
@ -36,9 +39,9 @@ class EditBucketBottomSheet {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: Colors.blueAccent, width: 1.5),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
borderSide: BorderSide(color: Colors.blueAccent, width: 1.5),
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
@ -46,256 +49,183 @@ class EditBucketBottomSheet {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleSubmit() async {
|
||||
final newName = nameController.text.trim();
|
||||
final newDesc = descController.text.trim();
|
||||
final newEmployeeIds = selectedIds.toList()..sort();
|
||||
final originalEmployeeIds = [...bucket.employeeIds]..sort();
|
||||
|
||||
final nameChanged = newName != bucket.name;
|
||||
final descChanged = newDesc != bucket.description;
|
||||
final employeeChanged =
|
||||
!(const ListEquality().equals(newEmployeeIds, originalEmployeeIds));
|
||||
|
||||
if (!nameChanged && !descChanged && !employeeChanged) {
|
||||
showAppSnackbar(
|
||||
title: "No Changes",
|
||||
message: "No changes were made to update the bucket.",
|
||||
type: SnackbarType.warning,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final success = await controller.updateBucket(
|
||||
id: bucket.id,
|
||||
name: newName,
|
||||
description: newDesc,
|
||||
employeeIds: newEmployeeIds,
|
||||
originalEmployeeIds: originalEmployeeIds,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
final directoryController = Get.find<DirectoryController>();
|
||||
await directoryController.fetchBuckets();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _formContent() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: nameController,
|
||||
decoration: _inputDecoration('Bucket Name'),
|
||||
),
|
||||
MySpacing.height(16),
|
||||
TextField(
|
||||
controller: descController,
|
||||
maxLines: 2,
|
||||
decoration: _inputDecoration('Description'),
|
||||
),
|
||||
MySpacing.height(20),
|
||||
MyText.labelLarge('Shared With', fontWeight: 600),
|
||||
MySpacing.height(8),
|
||||
Obx(() => TextField(
|
||||
controller: searchController,
|
||||
onChanged: (value) => searchText.value = value.toLowerCase(),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search employee...',
|
||||
prefixIcon: const Icon(Icons.search, size: 20),
|
||||
suffixIcon: searchText.value.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear, size: 18),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
searchText.value = '';
|
||||
},
|
||||
)
|
||||
: null,
|
||||
isDense: true,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade100,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
),
|
||||
)),
|
||||
MySpacing.height(8),
|
||||
Obx(() {
|
||||
final filtered = allEmployees.where((emp) {
|
||||
final fullName = '${emp.firstName} ${emp.lastName}'.toLowerCase();
|
||||
return fullName.contains(searchText.value);
|
||||
}).toList();
|
||||
|
||||
return SizedBox(
|
||||
height: 180,
|
||||
child: ListView.separated(
|
||||
itemCount: filtered.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 2),
|
||||
itemBuilder: (context, index) {
|
||||
final emp = filtered[index];
|
||||
final fullName = '${emp.firstName} ${emp.lastName}'.trim();
|
||||
|
||||
return Obx(() => Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
unselectedWidgetColor: Colors.grey.shade500,
|
||||
checkboxTheme: CheckboxThemeData(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4)),
|
||||
side: const BorderSide(color: Colors.grey),
|
||||
fillColor:
|
||||
MaterialStateProperty.resolveWith((states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return Colors.blueAccent;
|
||||
}
|
||||
return Colors.white;
|
||||
}),
|
||||
checkColor: MaterialStateProperty.all(Colors.white),
|
||||
),
|
||||
),
|
||||
child: CheckboxListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
value: selectedIds.contains(emp.id),
|
||||
onChanged: emp.id == ownerId
|
||||
? null
|
||||
: (val) {
|
||||
if (val == true) {
|
||||
selectedIds.add(emp.id);
|
||||
} else {
|
||||
selectedIds.remove(emp.id);
|
||||
}
|
||||
},
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyText.bodyMedium(
|
||||
fullName.isNotEmpty ? fullName : 'Unnamed',
|
||||
fontWeight: 600,
|
||||
),
|
||||
),
|
||||
if (emp.id == ownerId)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 6),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: emp.jobRole.isNotEmpty
|
||||
? MyText.bodySmall(
|
||||
emp.jobRole,
|
||||
color: Colors.grey.shade600,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) {
|
||||
return SingleChildScrollView(
|
||||
padding: MediaQuery.of(context).viewInsets,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 12,
|
||||
offset: Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
MySpacing.height(12),
|
||||
Center(
|
||||
child: MyText.titleMedium('Edit Bucket', fontWeight: 700),
|
||||
),
|
||||
MySpacing.height(24),
|
||||
|
||||
// Bucket Name
|
||||
TextField(
|
||||
controller: nameController,
|
||||
decoration: _inputDecoration('Bucket Name'),
|
||||
),
|
||||
MySpacing.height(16),
|
||||
|
||||
// Description
|
||||
TextField(
|
||||
controller: descController,
|
||||
maxLines: 2,
|
||||
decoration: _inputDecoration('Description'),
|
||||
),
|
||||
MySpacing.height(20),
|
||||
|
||||
// Shared With
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: MyText.labelLarge('Shared With', fontWeight: 600),
|
||||
),
|
||||
MySpacing.height(8),
|
||||
|
||||
// Search
|
||||
Obx(() => TextField(
|
||||
controller: searchController,
|
||||
onChanged: (value) =>
|
||||
searchText.value = value.toLowerCase(),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search employee...',
|
||||
prefixIcon: const Icon(Icons.search, size: 20),
|
||||
suffixIcon: searchText.value.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear, size: 18),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
searchText.value = '';
|
||||
},
|
||||
)
|
||||
: null,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade100,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
borderSide:
|
||||
BorderSide(color: Colors.blueAccent, width: 1.5),
|
||||
),
|
||||
),
|
||||
)),
|
||||
MySpacing.height(8),
|
||||
|
||||
// Employee list
|
||||
Obx(() {
|
||||
final filtered = allEmployees.where((emp) {
|
||||
final fullName =
|
||||
'${emp.firstName} ${emp.lastName}'.toLowerCase();
|
||||
return fullName.contains(searchText.value);
|
||||
}).toList();
|
||||
|
||||
return SizedBox(
|
||||
height: 180,
|
||||
child: ListView.builder(
|
||||
itemCount: filtered.length,
|
||||
itemBuilder: (context, index) {
|
||||
final emp = filtered[index];
|
||||
final fullName =
|
||||
'${emp.firstName} ${emp.lastName}'.trim();
|
||||
|
||||
return Obx(() => Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
checkboxTheme: CheckboxThemeData(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
side: const BorderSide(
|
||||
color: Colors.black, width: 2),
|
||||
fillColor:
|
||||
MaterialStateProperty.resolveWith<Color>(
|
||||
(states) {
|
||||
if (states
|
||||
.contains(MaterialState.selected)) {
|
||||
return Colors.blueAccent;
|
||||
}
|
||||
return Colors.transparent;
|
||||
}),
|
||||
checkColor:
|
||||
MaterialStateProperty.all(Colors.white),
|
||||
),
|
||||
),
|
||||
child: CheckboxListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity:
|
||||
const VisualDensity(vertical: -4),
|
||||
controlAffinity:
|
||||
ListTileControlAffinity.leading,
|
||||
value: selectedIds.contains(emp.id),
|
||||
onChanged: emp.id == ownerId
|
||||
? null
|
||||
: (val) {
|
||||
if (val == true) {
|
||||
selectedIds.add(emp.id);
|
||||
} else {
|
||||
selectedIds.remove(emp.id);
|
||||
}
|
||||
},
|
||||
title: Text(
|
||||
fullName.isNotEmpty ? fullName : 'Unnamed',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
MySpacing.height(24),
|
||||
|
||||
// Action Buttons
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => Get.back(),
|
||||
icon: const Icon(Icons.close, color: Colors.red),
|
||||
label: MyText.bodyMedium("Cancel",
|
||||
color: Colors.red, fontWeight: 600),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: const BorderSide(color: Colors.red),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
final newName = nameController.text.trim();
|
||||
final newDesc = descController.text.trim();
|
||||
final newEmployeeIds = selectedIds.toList()..sort();
|
||||
final originalEmployeeIds = [...bucket.employeeIds]
|
||||
..sort();
|
||||
|
||||
final nameChanged = newName != bucket.name;
|
||||
final descChanged = newDesc != bucket.description;
|
||||
final employeeChanged = !(ListEquality()
|
||||
.equals(newEmployeeIds, originalEmployeeIds));
|
||||
|
||||
if (!nameChanged &&
|
||||
!descChanged &&
|
||||
!employeeChanged) {
|
||||
showAppSnackbar(
|
||||
title: "No Changes",
|
||||
message:
|
||||
"No changes were made to update the bucket.",
|
||||
type: SnackbarType.warning,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final success = await controller.updateBucket(
|
||||
id: bucket.id,
|
||||
name: newName,
|
||||
description: newDesc,
|
||||
employeeIds: newEmployeeIds,
|
||||
originalEmployeeIds: originalEmployeeIds,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
final directoryController =
|
||||
Get.find<DirectoryController>();
|
||||
await directoryController.fetchBuckets();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.check_circle_outline,
|
||||
color: Colors.white),
|
||||
label: MyText.bodyMedium("Save",
|
||||
color: Colors.white, fontWeight: 600),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.indigo,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
return BaseBottomSheet(
|
||||
title: "Edit Bucket",
|
||||
onCancel: () => Navigator.pop(context),
|
||||
onSubmit: _handleSubmit,
|
||||
child: _formContent(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user