From 33d267f18efea5604c23ebb90576175ad2ca9675 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Fri, 11 Jul 2025 17:28:22 +0530 Subject: [PATCH] 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: [