From d0cbfa987d0023ef849f7d5f4597f36086950667 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Fri, 1 Aug 2025 17:26:11 +0530 Subject: [PATCH] feat: Refactor TeamMembersBottomSheet and CreateBucketBottomSheet for improved structure and readability --- .../widgets/team_members_bottom_sheet.dart | 342 +++++++------- .../directory/create_bucket_bottom_sheet.dart | 157 ++----- .../directory_filter_bottom_sheet.dart | 2 +- .../directory/edit_bucket_bottom_sheet.dart | 442 ++++++++---------- 4 files changed, 396 insertions(+), 547 deletions(-) diff --git a/lib/helpers/widgets/team_members_bottom_sheet.dart b/lib/helpers/widgets/team_members_bottom_sheet.dart index 6926df5..ff1ff5c 100644 --- a/lib/helpers/widgets/team_members_bottom_sheet.dart +++ b/lib/helpers/widgets/team_members_bottom_sheet.dart @@ -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 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, + ), + ); + }, + ); + } } diff --git a/lib/model/directory/create_bucket_bottom_sheet.dart b/lib/model/directory/create_bucket_bottom_sheet.dart index 51495c3..212035d 100644 --- a/lib/model/directory/create_bucket_bottom_sheet.dart +++ b/lib/model/directory/create_bucket_bottom_sheet.dart @@ -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 { ); } + 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( 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(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, ), ); }, diff --git a/lib/model/directory/directory_filter_bottom_sheet.dart b/lib/model/directory/directory_filter_bottom_sheet.dart index 6f10473..ea85e09 100644 --- a/lib/model/directory/directory_filter_bottom_sheet.dart +++ b/lib/model/directory/directory_filter_bottom_sheet.dart @@ -161,7 +161,7 @@ class _DirectoryFilterBottomSheetState ), const SizedBox(width: 4), MyText( - "$title (${selectedItems.length})", + "$title", fontWeight: 600, fontSize: 16, ), diff --git a/lib/model/directory/edit_bucket_bottom_sheet.dart b/lib/model/directory/edit_bucket_bottom_sheet.dart index f9bb4dc..7f31992 100644 --- a/lib/model/directory/edit_bucket_bottom_sheet.dart +++ b/lib/model/directory/edit_bucket_bottom_sheet.dart @@ -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 allEmployees, - {required String ownerId}) { + static void show( + BuildContext context, + ContactBucket bucket, + List 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 _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(); + 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( - (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(); - 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(), ); }, );