From 072bc34cdedd6ed0c7ccdb47d31fdba3a3471ee1 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Wed, 16 Jul 2025 10:31:09 +0530 Subject: [PATCH] Added Delete button with permission control --- .../directory/manage_bucket_controller.dart | 42 +- lib/helpers/services/api_service.dart | 38 +- lib/view/directory/manage_bucket_screen.dart | 390 +++++++++++------- 3 files changed, 311 insertions(+), 159 deletions(-) diff --git a/lib/controller/directory/manage_bucket_controller.dart b/lib/controller/directory/manage_bucket_controller.dart index d056a17..b4dd29e 100644 --- a/lib/controller/directory/manage_bucket_controller.dart +++ b/lib/controller/directory/manage_bucket_controller.dart @@ -3,11 +3,14 @@ import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/model/employee_model.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; +import 'package:marco/controller/directory/directory_controller.dart'; class ManageBucketController extends GetxController { RxList allEmployees = [].obs; RxBool isLoading = false.obs; + final DirectoryController directoryController = Get.find(); + @override void onInit() { super.onInit(); @@ -42,7 +45,6 @@ class ManageBucketController extends GetxController { return false; } - // Build payload: mark new ones active, removed ones inactive final allInvolvedIds = {...originalEmployeeIds, ...employeeIds}.toList(); final assignPayload = allInvolvedIds.map((empId) { @@ -93,8 +95,8 @@ class ManageBucketController extends GetxController { try { final response = await ApiService.getAllEmployees(); if (response != null && response.isNotEmpty) { - allEmployees - .assignAll(response.map((json) => EmployeeModel.fromJson(json))); + allEmployees.assignAll( + response.map((json) => EmployeeModel.fromJson(json))); logSafe( "All Employees fetched for Manage Bucket: ${allEmployees.length}", level: LogLevel.info, @@ -113,4 +115,38 @@ class ManageBucketController extends GetxController { isLoading.value = false; update(); } + + Future deleteBucket(String bucketId) async { + isLoading.value = true; + update(); + + try { + final deleted = await ApiService.deleteBucket(bucketId); + if (deleted) { + directoryController.contactBuckets.removeWhere((b) => b.id == bucketId); + showAppSnackbar( + title: "Deleted", + message: "Bucket deleted successfully.", + type: SnackbarType.success, + ); + } else { + showAppSnackbar( + title: "Delete Failed", + message: "Unable to delete bucket.", + type: SnackbarType.error, + ); + } + } catch (e, stack) { + logSafe("Error deleting bucket: $e", level: LogLevel.error); + logSafe("Stack: $stack", level: LogLevel.debug); + showAppSnackbar( + title: "Unexpected Error", + message: "Failed to delete bucket.", + type: SnackbarType.error, + ); + } finally { + isLoading.value = false; + update(); + } + } } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 23d3e7b..260e2b0 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -246,9 +246,45 @@ class ApiService { } /// Directory calling the API + + static Future deleteBucket(String id) async { + final endpoint = "${ApiEndpoints.updateBucket}/$id"; + try { + final token = await _getToken(); + if (token == null) { + logSafe("Token is null. Cannot proceed with DELETE request.", + level: LogLevel.error); + return false; + } + + final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint"); + + logSafe("Sending DELETE request to $uri", level: LogLevel.debug); + + final response = + await http.delete(uri, headers: _headers(token)).timeout(timeout); + + logSafe("DELETE bucket response status: ${response.statusCode}"); + logSafe("DELETE bucket response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (response.statusCode == 200 && json['success'] == true) { + logSafe("Bucket deleted successfully."); + return true; + } else { + logSafe( + "Failed to delete bucket: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.warning); + } + } catch (e, stack) { + logSafe("Exception during deleteBucket API: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return false; + } - /// Update an existing bucket by ID static Future updateBucket({ required String id, required String name, diff --git a/lib/view/directory/manage_bucket_screen.dart b/lib/view/directory/manage_bucket_screen.dart index 6cdd8ef..3c63ec4 100644 --- a/lib/view/directory/manage_bucket_screen.dart +++ b/lib/view/directory/manage_bucket_screen.dart @@ -94,156 +94,237 @@ class _ManageBucketsScreenState extends State { ), ), ), - 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), + body: SafeArea( + child: 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), + ), ), ), ), ), - ), - 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(); + 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 (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; - - final matchedEmployees = manageBucketController.allEmployees - .where((emp) => bucket.employeeIds.contains(emp.id)) - .toList(); - - return GestureDetector( - onTap: () { - TeamMembersBottomSheet.show( - context, - bucket, - matchedEmployees, - canEdit: canEdit, - onEdit: () { - Get.back(); // Close the bottom sheet first - Future.delayed(const Duration(milliseconds: 300), () { - EditBucketBottomSheet.show(context, bucket, manageBucketController.allEmployees); - }); -}, - ); - }, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, + if (buckets.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, 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, - ), - MySpacing.width(12), - Row( - children: [ - const Icon(Icons.ios_share_outlined, - size: 14, color: Colors.grey), - MySpacing.width(4), - MyText.labelSmall( - 'Shared with (${matchedEmployees.length})', - color: Colors.indigo, - fontWeight: 600, - ), - ], - ), - ], - ), - if (bucket.description.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 4), - child: LayoutBuilder( + 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; + + final matchedEmployees = manageBucketController.allEmployees + .where((emp) => bucket.employeeIds.contains(emp.id)) + .toList(); + + return GestureDetector( + onTap: () { + TeamMembersBottomSheet.show( + context, + bucket, + matchedEmployees, + canEdit: canEdit, + onEdit: () { + Get.back(); + Future.delayed(const Duration(milliseconds: 300), + () { + EditBucketBottomSheet.show(context, bucket, + manageBucketController.allEmployees); + }); + }, + ); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + Expanded( + child: MyText.titleSmall( + bucket.name, + fontWeight: 700, + overflow: TextOverflow.ellipsis, + ), + ), + if (canEdit) + IconButton( + icon: const Icon( + Icons.delete_outline, + color: Colors.red), + onPressed: () { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular( + 16), + ), + title: Row( + children: const [ + Icon( + Icons + .warning_amber_rounded, + color: Colors.red), + SizedBox(width: 8), + Text("Delete Bucket", + style: TextStyle( + fontWeight: + FontWeight + .bold)), + ], + ), + content: const Text( + "Are you sure you want to delete this bucket? This action cannot be undone.", + style: + TextStyle(fontSize: 15), + ), + actionsPadding: + const EdgeInsets + .symmetric( + horizontal: 12, + vertical: 8), + actions: [ + TextButton( + onPressed: () => + Navigator.of(ctx) + .pop(), + child: + const Text("Cancel"), + ), + ElevatedButton.icon( + style: ElevatedButton + .styleFrom( + foregroundColor: + Colors.white, + backgroundColor: + Colors.red, + shape: + RoundedRectangleBorder( + borderRadius: + BorderRadius + .circular(8), + ), + ), + icon: const Icon( + Icons.delete), + label: + const Text("Delete"), + onPressed: () { + Navigator.of(ctx).pop(); + manageBucketController + .deleteBucket( + bucket.id); + }, + ), + ], + ), + ); + }, + ) + ], + ), + if (!canEdit) MySpacing.height(12), + ], + ), + 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), + const Icon(Icons.ios_share_outlined, + size: 14, color: Colors.grey), + MySpacing.width(4), + MyText.labelSmall( + 'Shared with (${matchedEmployees.length})', + color: Colors.indigo, + fontWeight: 600, + ), + ], + ), + MySpacing.height(4), + if (bucket.description.isNotEmpty) + LayoutBuilder( builder: (context, constraints) { final span = TextSpan( text: bucket.description, @@ -257,7 +338,6 @@ class _ManageBucketsScreenState extends State { maxLines: 2, textDirection: TextDirection.ltr, )..layout(maxWidth: constraints.maxWidth); - final hasOverflow = tp.didExceedMaxLines; return Column( @@ -296,18 +376,18 @@ class _ManageBucketsScreenState extends State { ); }, ), - ), - ], + ], + ), ), - ), - ], - ), - ); - }, - ); - }), - ), - ], + ], + ), + ); + }, + ); + }), + ), + ], + ), ), ); }