From 33d267f18efea5604c23ebb90576175ad2ca9675 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Fri, 11 Jul 2025 17:28:22 +0530 Subject: [PATCH 01/10] feat(bucket): implement create bucket functionality with UI and API integration --- .../directory/add_contact_controller.dart | 4 +- .../directory/create_bucket_controller.dart | 70 +++++++ lib/helpers/services/api_endpoints.dart | 1 + lib/helpers/services/api_service.dart | 45 ++++- .../directory/create_bucket_bottom_sheet.dart | 171 ++++++++++++++++++ lib/view/directory/directory_view.dart | 99 +++++++++- 6 files changed, 384 insertions(+), 6 deletions(-) create mode 100644 lib/controller/directory/create_bucket_controller.dart create mode 100644 lib/model/directory/create_bucket_bottom_sheet.dart diff --git a/lib/controller/directory/add_contact_controller.dart b/lib/controller/directory/add_contact_controller.dart index bcc1abc..463d2e8 100644 --- a/lib/controller/directory/add_contact_controller.dart +++ b/lib/controller/directory/add_contact_controller.dart @@ -1,5 +1,3 @@ -// Updated AddContactController to support multiple emails and phones - import 'package:get/get.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/services/app_logger.dart'; @@ -75,7 +73,7 @@ class AddContactController extends GetxController { } catch (e) { logSafe("Failed to fetch buckets: \$e", level: LogLevel.error); } - } + } Future fetchOrganizationNames() async { try { diff --git a/lib/controller/directory/create_bucket_controller.dart b/lib/controller/directory/create_bucket_controller.dart new file mode 100644 index 0000000..80338d5 --- /dev/null +++ b/lib/controller/directory/create_bucket_controller.dart @@ -0,0 +1,70 @@ +import 'package:get/get.dart'; +import 'package:marco/helpers/services/api_service.dart'; +import 'package:marco/helpers/services/app_logger.dart'; +import 'package:marco/helpers/widgets/my_snackbar.dart'; + +class BucketController extends GetxController { + RxBool isCreating = false.obs; + final RxString name = ''.obs; + final RxString description = ''.obs; + + Future createBucket() async { + if (name.value.trim().isEmpty) { + showAppSnackbar( + title: "Missing Name", + message: "Bucket name is required.", + type: SnackbarType.warning, + ); + return; + } + + isCreating.value = true; + + try { + logSafe("Creating bucket: ${name.value}"); + + final success = await ApiService.createBucket( + name: name.value.trim(), + description: description.value.trim(), + ); + + if (success) { + logSafe("Bucket created successfully"); + + Get.back(result: true); // Close bottom sheet/dialog + + showAppSnackbar( + title: "Success", + message: "Bucket has been created successfully.", + type: SnackbarType.success, + ); + } else { + logSafe("Bucket creation failed", level: LogLevel.error); + showAppSnackbar( + title: "Creation Failed", + message: "Unable to create bucket. Please try again later.", + type: SnackbarType.error, + ); + } + } catch (e) { + logSafe("Error during bucket creation: $e", level: LogLevel.error); + showAppSnackbar( + title: "Unexpected Error", + message: "Something went wrong. Please try again.", + type: SnackbarType.error, + ); + } finally { + isCreating.value = false; + } + } + + void updateName(String value) { + name.value = value; + logSafe("Bucket name updated: ${value.trim()}"); + } + + void updateDescription(String value) { + description.value = value; + logSafe("Bucket description updated: ${value.trim()}"); + } +} diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 4dbeec6..bb7c500 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -43,4 +43,5 @@ class ApiEndpoints { static const String updateContact = "/directory"; static const String getDirectoryNotes = "/directory/notes"; static const String updateDirectoryNotes = "/directory/note"; + static const String createBucket = "/directory/bucket"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 75a3abf..411c594 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -163,7 +163,7 @@ class ApiService { final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint"); logSafe("POST $uri\nHeaders: ${_headers(token)}\nBody: $body", - ); + ); try { final response = await http @@ -243,6 +243,49 @@ class ApiService { } /// Directory calling the API + + static Future createBucket({ + required String name, + required String description, + }) async { + final payload = { + "name": name, + "description": description, + }; + + final endpoint = ApiEndpoints.createBucket; + + logSafe("Creating bucket with payload: $payload"); + + try { + final response = await _postRequest(endpoint, payload); + + if (response == null) { + logSafe("Create bucket failed: null response", level: LogLevel.error); + return false; + } + + logSafe("Create bucket response status: ${response.statusCode}"); + logSafe("Create bucket response body: ${response.body}"); + + final json = jsonDecode(response.body); + + if (json['success'] == true) { + logSafe("Bucket created successfully: ${json['data']}"); + return true; + } else { + logSafe("Failed to create bucket: ${json['message']}", + level: LogLevel.warning); + } + } catch (e, stack) { + logSafe("Exception during createBucket API: ${e.toString()}", + level: LogLevel.error); + logSafe("StackTrace: ${stack.toString()}", level: LogLevel.debug); + } + + return false; + } + static Future?> getDirectoryNotes({ int pageSize = 1000, int pageNumber = 1, diff --git a/lib/model/directory/create_bucket_bottom_sheet.dart b/lib/model/directory/create_bucket_bottom_sheet.dart new file mode 100644 index 0000000..51495c3 --- /dev/null +++ b/lib/model/directory/create_bucket_bottom_sheet.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/directory/create_bucket_controller.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'; + +class CreateBucketBottomSheet extends StatefulWidget { + const CreateBucketBottomSheet({super.key}); + + @override + State createState() => _CreateBucketBottomSheetState(); +} + +class _CreateBucketBottomSheetState extends State { + final BucketController _controller = Get.put(BucketController()); + final _formKey = GlobalKey(); + + InputDecoration _inputDecoration(String hint) { + return InputDecoration( + hintText: hint, + hintStyle: MyTextStyle.bodySmall(xMuted: true), + filled: true, + fillColor: Colors.grey.shade100, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.blueAccent, width: 1.5), + ), + contentPadding: MySpacing.all(16), + ); + } + + @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), + ), + ); + }), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + } + + @override + void dispose() { + Get.delete(); + super.dispose(); + } +} diff --git a/lib/view/directory/directory_view.dart b/lib/view/directory/directory_view.dart index 135da9f..b2e3807 100644 --- a/lib/view/directory/directory_view.dart +++ b/lib/view/directory/directory_view.dart @@ -11,6 +11,8 @@ 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'; class DirectoryView extends StatelessWidget { final DirectoryController controller = Get.find(); @@ -32,11 +34,104 @@ class DirectoryView extends StatelessWidget { 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"), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + + 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(); } @@ -282,8 +377,8 @@ class DirectoryView extends StatelessWidget { ), )), ...contact.contactPhones.map((p) => Padding( - padding: - const EdgeInsets.only(bottom: 8,top: 4), + padding: const EdgeInsets.only( + bottom: 8, top: 4), child: Row( mainAxisSize: MainAxisSize.min, children: [ From 574e7df447cd59424c524856a3e98733ac9f448d Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 14 Jul 2025 10:03:27 +0530 Subject: [PATCH 02/10] feat(contact): streamline validation logic and enhance UI for adding contacts --- .../directory/add_contact_controller.dart | 84 +--- .../directory/add_contact_bottom_sheet.dart | 369 +++++++++--------- 2 files changed, 204 insertions(+), 249 deletions(-) diff --git a/lib/controller/directory/add_contact_controller.dart b/lib/controller/directory/add_contact_controller.dart index 463d2e8..3cfcc57 100644 --- a/lib/controller/directory/add_contact_controller.dart +++ b/lib/controller/directory/add_contact_controller.dart @@ -73,7 +73,7 @@ class AddContactController extends GetxController { } catch (e) { logSafe("Failed to fetch buckets: \$e", level: LogLevel.error); } - } + } Future fetchOrganizationNames() async { try { @@ -101,7 +101,7 @@ class AddContactController extends GetxController { .whereType() .toList(); - // === Per-field Validation with Specific Messages === + // === Required validations only for name, organization, and bucket === if (name.trim().isEmpty) { showAppSnackbar( title: "Missing Name", @@ -120,51 +120,6 @@ class AddContactController extends GetxController { return; } - if (emails.isEmpty) { - showAppSnackbar( - title: "Missing Email", - message: "Please add at least one email.", - type: SnackbarType.warning, - ); - return; - } - - if (phones.isEmpty) { - showAppSnackbar( - title: "Missing Phone Number", - message: "Please add at least one phone number.", - type: SnackbarType.warning, - ); - return; - } - - if (address.trim().isEmpty) { - showAppSnackbar( - title: "Missing Address", - message: "Please enter the address.", - type: SnackbarType.warning, - ); - return; - } - - if (description.trim().isEmpty) { - showAppSnackbar( - title: "Missing Description", - message: "Please enter a description.", - type: SnackbarType.warning, - ); - return; - } - - if (selectedCategory.value.trim().isEmpty || categoryId == null) { - showAppSnackbar( - title: "Missing Category", - message: "Please select a contact category.", - type: SnackbarType.warning, - ); - return; - } - if (selectedBucket.value.trim().isEmpty || bucketId == null) { showAppSnackbar( title: "Missing Bucket", @@ -174,25 +129,7 @@ class AddContactController extends GetxController { return; } - if (selectedProjects.isEmpty || projectIds.isEmpty) { - showAppSnackbar( - title: "Missing Projects", - message: "Please select at least one project.", - type: SnackbarType.warning, - ); - return; - } - - if (enteredTags.isEmpty) { - showAppSnackbar( - title: "Missing Tags", - message: "Please enter at least one tag.", - type: SnackbarType.warning, - ); - return; - } - - // === Submit if all validations passed === + // === Build body (include optional fields if available) === try { final tagObjects = enteredTags.map((tagName) { final tagId = tagsMap[tagName]; @@ -205,14 +142,15 @@ class AddContactController extends GetxController { if (id != null) "id": id, "name": name.trim(), "organization": organization.trim(), - "contactCategoryId": categoryId, - "projectIds": projectIds, + if (selectedCategory.value.isNotEmpty && categoryId != null) + "contactCategoryId": categoryId, + if (projectIds.isNotEmpty) "projectIds": projectIds, "bucketIds": [bucketId], - "tags": tagObjects, - "contactEmails": emails, - "contactPhones": phones, - "address": address.trim(), - "description": description.trim(), + if (enteredTags.isNotEmpty) "tags": tagObjects, + if (emails.isNotEmpty) "contactEmails": emails, + if (phones.isNotEmpty) "contactPhones": phones, + if (address.trim().isNotEmpty) "address": address.trim(), + if (description.trim().isNotEmpty) "description": description.trim(), }; logSafe("${id != null ? 'Updating' : 'Creating'} contact"); diff --git a/lib/model/directory/add_contact_bottom_sheet.dart b/lib/model/directory/add_contact_bottom_sheet.dart index 97d2e95..77f9bce 100644 --- a/lib/model/directory/add_contact_bottom_sheet.dart +++ b/lib/model/directory/add_contact_bottom_sheet.dart @@ -25,7 +25,7 @@ class _AddContactBottomSheetState extends State { final addressController = TextEditingController(); final descriptionController = TextEditingController(); final tagTextController = TextEditingController(); - + final RxBool showAdvanced = false.obs; final RxList emailControllers = [].obs; final RxList emailLabels = [].obs; @@ -514,185 +514,202 @@ class _AddContactBottomSheetState extends State { borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), ), child: Padding( - padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), - child: Form( - key: formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: MyText.titleMedium( - widget.existingContact != null - ? "Edit Contact" - : "Create New Contact", - fontWeight: 700, - ), - ), - MySpacing.height(24), - _sectionLabel("Basic Info"), - MySpacing.height(16), - _buildTextField("Name", nameController), - MySpacing.height(16), - _buildOrganizationField(), - MySpacing.height(24), - _sectionLabel("Contact Info"), - MySpacing.height(16), - Obx(() => _buildEmailList()), - TextButton.icon( - onPressed: () { - emailControllers.add(TextEditingController()); - emailLabels.add('Office'.obs); - }, - icon: const Icon(Icons.add), - label: const Text("Add Email"), - ), - Obx(() => _buildPhoneList()), - TextButton.icon( - onPressed: () { - phoneControllers.add(TextEditingController()); - phoneLabels.add('Work'.obs); - }, - icon: const Icon(Icons.add), - label: const Text("Add Phone"), - ), - MySpacing.height(24), - _sectionLabel("Other Details"), - MySpacing.height(16), - MyText.labelMedium("Category"), - MySpacing.height(8), - _popupSelector( - hint: "Select Category", - selectedValue: controller.selectedCategory, - options: controller.categories, - ), - MySpacing.height(16), - MyText.labelMedium("Select Projects"), - MySpacing.height(8), - GestureDetector( - onTap: () async { - await showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Select Projects'), - content: Obx(() { - return SizedBox( - width: double.maxFinite, - child: ListView( - shrinkWrap: true, - children: - controller.globalProjects.map((project) { - final isSelected = controller - .selectedProjects - .contains(project); - return Theme( - data: Theme.of(context).copyWith( - unselectedWidgetColor: Colors - .black, // checkbox border when not selected - checkboxTheme: CheckboxThemeData( - fillColor: MaterialStateProperty - .resolveWith((states) { - if (states.contains( - MaterialState.selected)) { - return Colors - .white; // fill when selected - } - return Colors.transparent; - }), - checkColor: MaterialStateProperty.all( - Colors.black), // check mark color - side: const BorderSide( - color: Colors.black, - width: 2), // border color - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(4), - ), - ), - ), - child: CheckboxListTile( - dense: true, - title: Text(project), - value: isSelected, - onChanged: (bool? selected) { - if (selected == true) { - controller.selectedProjects - .add(project); - } else { - controller.selectedProjects - .remove(project); - } - }, - ), - ); - }).toList(), - ), - ); - }), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Done'), - ), - ], - ); - }, - ); - }, - child: Container( - height: 48, - padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey.shade300), + padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), + child: Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: MyText.titleMedium( + widget.existingContact != null + ? "Edit Contact" + : "Create New Contact", + fontWeight: 700, ), - alignment: Alignment.centerLeft, - child: Obx(() { - final selected = controller.selectedProjects; - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - selected.isEmpty - ? "Select Projects" - : selected.join(', '), - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14), - ), - ), - const Icon(Icons.expand_more, size: 20), - ], - ); - }), ), - ), - MySpacing.height(16), - MyText.labelMedium("Select Bucket"), - MySpacing.height(8), - _popupSelector( - hint: "Select Bucket", - selectedValue: controller.selectedBucket, - options: controller.buckets, - ), - MySpacing.height(16), - MyText.labelMedium("Tags"), - MySpacing.height(8), - _tagInputSection(), - MySpacing.height(16), - _buildTextField("Address", addressController, maxLines: 2), - MySpacing.height(16), - _buildTextField("Description", descriptionController, - maxLines: 2), - MySpacing.height(24), - _buildActionButtons(), - ], - ), - ), - ), + MySpacing.height(24), + _sectionLabel("Required Fields"), + MySpacing.height(12), + _buildTextField("Name", nameController), + MySpacing.height(16), + _buildOrganizationField(), + MySpacing.height(16), + MyText.labelMedium("Select Bucket"), + MySpacing.height(8), + _popupSelector( + hint: "Select Bucket", + selectedValue: controller.selectedBucket, + options: controller.buckets, + ), + MySpacing.height(24), + + // Toggle for Advanced Section + Obx(() => GestureDetector( + onTap: () => showAdvanced.toggle(), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyText.labelLarge("Advanced Details (Optional)", + fontWeight: 600), + Icon(showAdvanced.value + ? Icons.expand_less + : Icons.expand_more), + ], + ), + )), + Obx(() => showAdvanced.value + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MySpacing.height(24), + _sectionLabel("Contact Info"), + MySpacing.height(16), + _buildEmailList(), + TextButton.icon( + onPressed: () { + emailControllers.add(TextEditingController()); + emailLabels.add('Office'.obs); + }, + icon: const Icon(Icons.add), + label: const Text("Add Email"), + ), + _buildPhoneList(), + TextButton.icon( + onPressed: () { + phoneControllers.add(TextEditingController()); + phoneLabels.add('Work'.obs); + }, + icon: const Icon(Icons.add), + label: const Text("Add Phone"), + ), + MySpacing.height(24), + _sectionLabel("Other Details"), + MySpacing.height(16), + MyText.labelMedium("Category"), + MySpacing.height(8), + _popupSelector( + hint: "Select Category", + selectedValue: controller.selectedCategory, + options: controller.categories, + ), + MySpacing.height(16), + MyText.labelMedium("Select Projects"), + MySpacing.height(8), + _projectSelectorUI(), + MySpacing.height(16), + MyText.labelMedium("Tags"), + MySpacing.height(8), + _tagInputSection(), + MySpacing.height(16), + _buildTextField("Address", addressController, + maxLines: 2), + MySpacing.height(16), + _buildTextField( + "Description", descriptionController, + maxLines: 2), + ], + ) + : SizedBox()), + + MySpacing.height(24), + _buildActionButtons(), + ], + ), + )), ), ); }); } + + Widget _projectSelectorUI() { + return GestureDetector( + onTap: () async { + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Select Projects'), + content: Obx(() { + return SizedBox( + width: double.maxFinite, + child: ListView( + shrinkWrap: true, + children: controller.globalProjects.map((project) { + final isSelected = + controller.selectedProjects.contains(project); + return Theme( + data: Theme.of(context).copyWith( + unselectedWidgetColor: Colors.black, + checkboxTheme: CheckboxThemeData( + fillColor: MaterialStateProperty.resolveWith( + (states) { + if (states.contains(MaterialState.selected)) { + return Colors.white; + } + return Colors.transparent; + }), + checkColor: MaterialStateProperty.all(Colors.black), + side: + const BorderSide(color: Colors.black, width: 2), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + ), + child: CheckboxListTile( + dense: true, + title: Text(project), + value: isSelected, + onChanged: (bool? selected) { + if (selected == true) { + controller.selectedProjects.add(project); + } else { + controller.selectedProjects.remove(project); + } + }, + ), + ); + }).toList(), + ), + ); + }), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Done'), + ), + ], + ); + }, + ); + }, + child: Container( + height: 48, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + alignment: Alignment.centerLeft, + child: Obx(() { + final selected = controller.selectedProjects; + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + selected.isEmpty ? "Select Projects" : selected.join(', '), + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ), + ), + const Icon(Icons.expand_more, size: 20), + ], + ); + }), + ), + ); + } } From 395444e8fc7142f9c7d46176addf254c22473422 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 14 Jul 2025 10:12:40 +0530 Subject: [PATCH 03/10] feat(contact): add contact picker functionality for selecting Indian phone numbers --- lib/helpers/utils/contact_picker_helper.dart | 90 +++++++++++++++++++ .../directory/add_contact_bottom_sheet.dart | 57 +++++++----- 2 files changed, 126 insertions(+), 21 deletions(-) create mode 100644 lib/helpers/utils/contact_picker_helper.dart diff --git a/lib/helpers/utils/contact_picker_helper.dart b/lib/helpers/utils/contact_picker_helper.dart new file mode 100644 index 0000000..73c2840 --- /dev/null +++ b/lib/helpers/utils/contact_picker_helper.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_contacts/flutter_contacts.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:marco/helpers/widgets/my_snackbar.dart'; +import 'package:marco/helpers/services/app_logger.dart'; + +class ContactPickerHelper { + static Future pickIndianPhoneNumber(BuildContext context) async { + final status = await Permission.contacts.request(); + + if (!status.isGranted) { + if (status.isPermanentlyDenied) { + await openAppSettings(); + } + + showAppSnackbar( + title: "Permission Required", + message: + "Please allow Contacts permission from settings to pick a contact.", + type: SnackbarType.warning, + ); + return null; + } + + try { + final picked = await FlutterContacts.openExternalPick(); + if (picked == null) return null; + + final contact = + await FlutterContacts.getContact(picked.id, withProperties: true); + if (contact == null || contact.phones.isEmpty) { + showAppSnackbar( + title: "No Phone Number", + message: "Selected contact has no phone number.", + type: SnackbarType.warning, + ); + return null; + } + + final indiaPhones = contact.phones.where((p) { + final normalized = p.number.replaceAll(RegExp(r'[^0-9+]'), ''); + return normalized.startsWith('+91') || RegExp(r'^\d{10}$').hasMatch(normalized); + }).toList(); + + if (indiaPhones.isEmpty) { + showAppSnackbar( + title: "No Indian Number", + message: "Selected contact has no Indian (+91) phone number.", + type: SnackbarType.warning, + ); + return null; + } + + if (indiaPhones.length == 1) { + return _normalizeNumber(indiaPhones.first.number); + } + + return await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text("Choose a number"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: indiaPhones + .map((p) => ListTile( + title: Text(p.number), + onTap: () => Navigator.of(ctx).pop(_normalizeNumber(p.number)), + )) + .toList(), + ), + ), + ); + } catch (e, st) { + logSafe("Error picking contact", level: LogLevel.error, error: e, stackTrace: st); + showAppSnackbar( + title: "Error", + message: "Failed to fetch contact.", + type: SnackbarType.error, + ); + return null; + } + } + + static String _normalizeNumber(String raw) { + final normalized = raw.replaceAll(RegExp(r'[^0-9]'), ''); + return normalized.length > 10 + ? normalized.substring(normalized.length - 10) + : normalized; + } +} diff --git a/lib/model/directory/add_contact_bottom_sheet.dart b/lib/model/directory/add_contact_bottom_sheet.dart index 77f9bce..22b561b 100644 --- a/lib/model/directory/add_contact_bottom_sheet.dart +++ b/lib/model/directory/add_contact_bottom_sheet.dart @@ -7,6 +7,7 @@ import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/model/directory/contact_model.dart'; +import 'package:marco/helpers/utils/contact_picker_helper.dart'; class AddContactBottomSheet extends StatefulWidget { final ContactModel? existingContact; @@ -184,8 +185,23 @@ class _AddContactBottomSheetState extends State { inputFormatters: inputType == TextInputType.phone ? [FilteringTextInputFormatter.digitsOnly] : [], - decoration: _inputDecoration("Enter $inputLabel") - .copyWith(counterText: ""), + decoration: _inputDecoration("Enter $inputLabel").copyWith( + counterText: "", + suffixIcon: inputType == TextInputType.phone + ? IconButton( + icon: const Icon(Icons.contact_phone, + color: Colors.blue), + onPressed: () async { + final selectedPhone = + await ContactPickerHelper.pickIndianPhoneNumber( + context); + if (selectedPhone != null) { + controller.text = selectedPhone; + } + }, + ) + : null, + ), validator: (value) { if (value == null || value.trim().isEmpty) return "$inputLabel is required"; @@ -195,7 +211,6 @@ class _AddContactBottomSheetState extends State { return "Enter valid phone number"; } } - if (inputType == TextInputType.emailAddress && !RegExp(r'^[\w.-]+@([\w-]+\.)+[\w-]{2,4}$') .hasMatch(trimmed)) { @@ -243,24 +258,24 @@ class _AddContactBottomSheetState extends State { Widget _buildPhoneList() => Column( children: List.generate(phoneControllers.length, (index) { - return Padding( - padding: const EdgeInsets.only(bottom: 12), - child: _buildLabeledRow( - "Phone Label", - phoneLabels[index], - ["Work", "Mobile", "Other"], - "Phone", - phoneControllers[index], - TextInputType.phone, - onRemove: phoneControllers.length > 1 - ? () { - phoneControllers.removeAt(index); - phoneLabels.removeAt(index); - } - : null, - ), - ); - }), + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _buildLabeledRow( + "Phone Label", + phoneLabels[index], + ["Work", "Mobile", "Other"], + "Phone", + phoneControllers[index], + TextInputType.phone, + onRemove: phoneControllers.length > 1 + ? () { + phoneControllers.removeAt(index); + phoneLabels.removeAt(index); + } + : null, + ), + ); + }), ); Widget _popupSelector({ From 07bf9a93aaa03a669a211f31a6a83fb33afdf7e1 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 14 Jul 2025 10:15:11 +0530 Subject: [PATCH 04/10] refactor(add_contact): improve phone list rendering and enhance layout structure --- .../directory/add_contact_bottom_sheet.dart | 63 ++++++++++--------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/lib/model/directory/add_contact_bottom_sheet.dart b/lib/model/directory/add_contact_bottom_sheet.dart index 22b561b..a897fb1 100644 --- a/lib/model/directory/add_contact_bottom_sheet.dart +++ b/lib/model/directory/add_contact_bottom_sheet.dart @@ -258,24 +258,24 @@ class _AddContactBottomSheetState extends State { Widget _buildPhoneList() => Column( children: List.generate(phoneControllers.length, (index) { - return Padding( - padding: const EdgeInsets.only(bottom: 12), - child: _buildLabeledRow( - "Phone Label", - phoneLabels[index], - ["Work", "Mobile", "Other"], - "Phone", - phoneControllers[index], - TextInputType.phone, - onRemove: phoneControllers.length > 1 - ? () { - phoneControllers.removeAt(index); - phoneLabels.removeAt(index); - } - : null, - ), - ); - }), + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _buildLabeledRow( + "Phone Label", + phoneLabels[index], + ["Work", "Mobile", "Other"], + "Phone", + phoneControllers[index], + TextInputType.phone, + onRemove: phoneControllers.length > 1 + ? () { + phoneControllers.removeAt(index); + phoneLabels.removeAt(index); + } + : null, + ), + ); + }), ); Widget _popupSelector({ @@ -521,14 +521,16 @@ class _AddContactBottomSheetState extends State { return const Center(child: CircularProgressIndicator()); } - return SingleChildScrollView( - padding: MediaQuery.of(context).viewInsets, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), - ), - child: Padding( + return SafeArea( + child: SingleChildScrollView( + padding: MediaQuery.of(context).viewInsets, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Padding( padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), child: Form( key: formKey, @@ -558,8 +560,6 @@ class _AddContactBottomSheetState extends State { options: controller.buckets, ), MySpacing.height(24), - - // Toggle for Advanced Section Obx(() => GestureDetector( onTap: () => showAdvanced.toggle(), child: Row( @@ -625,13 +625,14 @@ class _AddContactBottomSheetState extends State { maxLines: 2), ], ) - : SizedBox()), - + : const SizedBox()), MySpacing.height(24), _buildActionButtons(), ], ), - )), + ), + ), + ), ), ); }); From f9ab336eb0505bcc4a8f4761c64290f63f3ed87c Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 14 Jul 2025 18:56:00 +0530 Subject: [PATCH 05/10] 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, + ), + ), + ), + ], + ); + }, + ), + ), + ], + ), + ), + ], + ); + }, + ); + }), + ); + } +} From 9c28dc05ddd6acd6725a404959af01f7ff6da14b Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Tue, 15 Jul 2025 10:52:36 +0530 Subject: [PATCH 06/10] feat(bucket): add search functionality to Manage Buckets screen --- lib/view/directory/manage_bucket_screen.dart | 423 +++++++++++-------- 1 file changed, 242 insertions(+), 181 deletions(-) diff --git a/lib/view/directory/manage_bucket_screen.dart b/lib/view/directory/manage_bucket_screen.dart index e38fc8f..31745ea 100644 --- a/lib/view/directory/manage_bucket_screen.dart +++ b/lib/view/directory/manage_bucket_screen.dart @@ -23,7 +23,17 @@ class _ManageBucketsScreenState extends State { final ManageBucketController manageBucketController = Get.put(ManageBucketController()); final ProjectController projectController = Get.find(); + final Map _expandedMap = {}; + final TextEditingController searchController = TextEditingController(); + String searchText = ''; + + void _clearSearch() { + searchController.clear(); + setState(() { + searchText = ''; + }); + } @override Widget build(BuildContext context) { @@ -55,28 +65,26 @@ class _ManageBucketsScreenState extends State { 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], - ), + 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], ), - ], - ); - }, - ), + ), + ], + ); + }), ], ), ), @@ -85,184 +93,237 @@ class _ManageBucketsScreenState extends State { ), ), ), - 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), + body: Column( + children: [ + Padding( + padding: MySpacing.xy(16, 12), + child: SizedBox( + height: 38, + child: TextField( + controller: searchController, + onChanged: (value) { + setState(() { + searchText = value.toLowerCase(); + }); + }, + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 0), + prefixIcon: const Icon(Icons.search, + size: 18, color: Colors.grey), + suffixIcon: searchText.isNotEmpty + ? IconButton( + icon: const Icon(Icons.close, color: Colors.grey), + onPressed: _clearSearch, + ) + : null, + hintText: 'Search buckets...', + filled: true, + fillColor: Colors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: Colors.grey.shade300), + ), ), - MySpacing.width(12), - Expanded( + ), + ), + ), + Expanded( + child: Obx(() { + final buckets = directoryController.contactBuckets.where((bucket) { + return bucket.name.toLowerCase().contains(searchText) || + bucket.description.toLowerCase().contains(searchText) || + bucket.numberOfContacts + .toString() + .contains(searchText); + }).toList(); + + 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, 0, 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: [ - 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); - }, - ), - ], + const Padding( + padding: EdgeInsets.only(top: 4), + child: Icon(Icons.label_outline, + size: 26, color: Colors.indigo), ), - 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), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - const Icon(Icons.ios_share_outlined, + 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( - 'Shared with', - color: Colors.indigo, + '${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); + 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, + ), + ), + ), + ], + ); + }, + ), + ), + ], ), + ), ], - ), - ), - ], - ); - }, - ); - }), + ); + }, + ); + }), + ), + ], + ), ); } } From 219815dd279a151b43abe429782cdedf6f2b5493 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Tue, 15 Jul 2025 12:56:58 +0530 Subject: [PATCH 07/10] refactor(directory): simplify floating action button and enhance menu structure --- lib/view/directory/directory_view.dart | 160 ++++++++++++++----------- 1 file changed, 87 insertions(+), 73 deletions(-) diff --git a/lib/view/directory/directory_view.dart b/lib/view/directory/directory_view.dart index 9c702a5..aca2692 100644 --- a/lib/view/directory/directory_view.dart +++ b/lib/view/directory/directory_view.dart @@ -25,7 +25,6 @@ class DirectoryView extends StatefulWidget { class _DirectoryViewState extends State { final DirectoryController controller = Get.find(); final TextEditingController searchController = TextEditingController(); - bool isFabExpanded = false; final PermissionController permissionController = Get.put(PermissionController()); @@ -148,57 +147,11 @@ class _DirectoryViewState extends State { Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, - 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(); - }, - ), - ); - } 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), - ), - ], + floatingActionButton: FloatingActionButton( + heroTag: 'createContact', + backgroundColor: Colors.red, + onPressed: _handleCreateContact, + child: const Icon(Icons.person_add_alt_1, color: Colors.white), ), body: Column( children: [ @@ -312,28 +265,89 @@ class _DirectoryViewState extends State { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), - itemBuilder: (context) => [ - PopupMenuItem( - value: 0, - enabled: false, - child: Obx(() => Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - MyText.bodySmall('Show Inactive', - fontWeight: 600), - Switch.adaptive( - value: !controller.isActive.value, - activeColor: Colors.indigo, - onChanged: (val) { - controller.isActive.value = !val; - controller.fetchContacts(active: !val); - Navigator.pop(context); - }, - ), + 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), + ), + ), + ); + + 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( + const PopupMenuItem( + enabled: false, + height: 30, + child: Text( + "Preferences", + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.grey), + ), + ), + ); + + menuItems.add( + PopupMenuItem( + value: 0, + enabled: false, + child: Obx(() => Row( + children: [ + const Icon(Icons.visibility_off_outlined, + size: 20, color: Colors.black87), + const SizedBox(width: 10), + const Expanded(child: Text('Show Inactive')), + Switch.adaptive( + value: !controller.isActive.value, + activeColor: Colors.indigo, + onChanged: (val) { + controller.isActive.value = !val; + controller.fetchContacts(active: !val); + Navigator.pop(context); + }, + ), + ], + )), + ), + ); + + return menuItems; + }, ), ), ], From e624fb00a0bf051c50b8acb27dd786db6e6e28ed Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Tue, 15 Jul 2025 14:52:25 +0530 Subject: [PATCH 08/10] feat(team_members): enhance Team Members Bottom Sheet with bucket details and edit functionality --- .../widgets/team_members_bottom_sheet.dart | 176 ++++++++++++- lib/view/directory/directory_view.dart | 72 +++--- lib/view/directory/manage_bucket_screen.dart | 243 ++++++++---------- 3 files changed, 313 insertions(+), 178 deletions(-) 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, + ), ), ), - ), - ], - ); - }, + ], + ); + }, + ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ); }, ); From 0335b0d3ab147132d44308b31e885b139219cce3 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Tue, 15 Jul 2025 14:56:44 +0530 Subject: [PATCH 09/10] fix(add_contact): adjust padding in Add Contact Bottom Sheet for better layout --- lib/model/directory/add_contact_bottom_sheet.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/model/directory/add_contact_bottom_sheet.dart b/lib/model/directory/add_contact_bottom_sheet.dart index a897fb1..31e0f4a 100644 --- a/lib/model/directory/add_contact_bottom_sheet.dart +++ b/lib/model/directory/add_contact_bottom_sheet.dart @@ -523,7 +523,9 @@ class _AddContactBottomSheetState extends State { return SafeArea( child: SingleChildScrollView( - padding: MediaQuery.of(context).viewInsets, + padding: EdgeInsets.only( + top: 32, + ).add(MediaQuery.of(context).viewInsets), child: Container( decoration: BoxDecoration( color: Theme.of(context).cardColor, From 9f7d6c92c583817212a323119379cae9a3de7460 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Tue, 15 Jul 2025 15:06:16 +0530 Subject: [PATCH 10/10] feat(manage_bucket): add dialog for upcoming edit feature in Team Members Bottom Sheet --- lib/view/directory/manage_bucket_screen.dart | 52 ++++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/lib/view/directory/manage_bucket_screen.dart b/lib/view/directory/manage_bucket_screen.dart index c12ae90..a5a0ce7 100644 --- a/lib/view/directory/manage_bucket_screen.dart +++ b/lib/view/directory/manage_bucket_screen.dart @@ -184,11 +184,57 @@ class _ManageBucketsScreenState extends State { onTap: () { TeamMembersBottomSheet.show( context, - bucket, + bucket, matchedEmployees, - canEdit: canEdit, + canEdit: canEdit, onEdit: () { - print('Edit bucket: ${bucket.name}'); + Get.defaultDialog( + title: '', + contentPadding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 16), + content: Column( + children: [ + const Icon(Icons.warning_amber, + size: 48, color: Colors.amber), + const SizedBox(height: 16), + Text( + 'Coming Soon', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 8), + Text( + 'This feature is under development and will be available soon.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Colors.grey[700], + ), + ), + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric( + vertical: 12), + ), + onPressed: () => Get.back(), + child: const Text('OK'), + ), + ), + ], + ), + radius: 12, + ); }, ); },