Vaibhav_Task-#742 #54

Merged
vaibhav.surve merged 4 commits from Vaibhav_Task-#742 into main 2025-07-16 06:25:43 +00:00
6 changed files with 822 additions and 280 deletions

View File

@ -2,35 +2,151 @@ 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';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/directory/directory_controller.dart';
class ManageBucketController extends GetxController {
RxList<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
RxBool isLoading = false.obs;
final DirectoryController directoryController = Get.find();
@override
void onInit() {
super.onInit();
fetchAllEmployees();
}
Future<bool> updateBucket({
required String id,
required String name,
required String description,
required List<String> employeeIds,
required List<String> originalEmployeeIds,
}) async {
isLoading(true);
update();
try {
final updated = await ApiService.updateBucket(
id: id,
name: name,
description: description,
);
if (!updated) {
showAppSnackbar(
title: "Update Failed",
message: "Unable to update bucket details.",
type: SnackbarType.error,
);
isLoading(false);
update();
return false;
}
final allInvolvedIds = {...originalEmployeeIds, ...employeeIds}.toList();
final assignPayload = allInvolvedIds.map((empId) {
return {
"employeeId": empId,
"isActive": employeeIds.contains(empId),
};
}).toList();
final assigned = await ApiService.assignEmployeesToBucket(
bucketId: id,
employees: assignPayload,
);
if (!assigned) {
showAppSnackbar(
title: "Assignment Failed",
message: "Employees couldn't be updated.",
type: SnackbarType.warning,
);
} else {
showAppSnackbar(
title: "Success",
message: "Bucket updated successfully.",
type: SnackbarType.success,
);
}
return true;
} catch (e, stack) {
logSafe("Error in updateBucket: $e", level: LogLevel.error);
logSafe("Stack: $stack", level: LogLevel.debug);
showAppSnackbar(
title: "Unexpected Error",
message: "Please try again later.",
type: SnackbarType.error,
);
return false;
} finally {
isLoading(false);
update();
}
}
Future<void> 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);
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);
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);
logSafe("Error fetching employees in Manage Bucket",
level: LogLevel.error, error: e);
}
isLoading.value = false;
update();
}
Future<void> 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();
}
}
}

View File

@ -44,4 +44,6 @@ class ApiEndpoints {
static const String getDirectoryNotes = "/directory/notes";
static const String updateDirectoryNotes = "/directory/note";
static const String createBucket = "/directory/bucket";
static const String updateBucket = "/directory/bucket";
static const String assignBucket = "/directory/assign-bucket";
}

View File

@ -162,7 +162,8 @@ class ApiService {
if (token == null) return null;
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint");
logSafe("POST $uri\nHeaders: ${_headers(token)}\nBody: $body",
logSafe(
"POST $uri\nHeaders: ${_headers(token)}\nBody: $body",
);
try {
@ -203,7 +204,9 @@ class ApiService {
if (additionalHeaders != null) ...additionalHeaders,
};
logSafe("PUT $uri\nHeaders: $headers\nBody: $body", );
logSafe(
"PUT $uri\nHeaders: $headers\nBody: $body",
);
try {
final response = await http
@ -244,6 +247,124 @@ class ApiService {
/// Directory calling the API
static Future<bool> 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;
}
static Future<bool> updateBucket({
required String id,
required String name,
required String description,
}) async {
final payload = {
"id": id,
"name": name,
"description": description,
};
final endpoint = "${ApiEndpoints.updateBucket}/$id";
logSafe("Updating bucket with payload: $payload");
try {
final response = await _putRequest(endpoint, payload);
if (response == null) {
logSafe("Update bucket failed: null response", level: LogLevel.error);
return false;
}
logSafe("Update bucket response status: ${response.statusCode}");
logSafe("Update bucket response body: ${response.body}");
final json = jsonDecode(response.body);
if (json['success'] == true) {
logSafe("Bucket updated successfully: ${json['data']}");
return true;
} else {
logSafe("Failed to update bucket: ${json['message']}",
level: LogLevel.warning);
}
} catch (e, stack) {
logSafe("Exception during updateBucket API: $e", level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
}
return false;
}
/// Assign employees to a bucket
static Future<bool> assignEmployeesToBucket({
required String bucketId,
required List<Map<String, dynamic>> employees,
}) async {
final endpoint = "${ApiEndpoints.assignBucket}/$bucketId";
logSafe("Assigning employees to bucket $bucketId: $employees");
try {
final response = await _postRequest(endpoint, employees);
if (response == null) {
logSafe("Assign employees failed: null response",
level: LogLevel.error);
return false;
}
logSafe("Assign employees response status: ${response.statusCode}");
logSafe("Assign employees response body: ${response.body}");
final json = jsonDecode(response.body);
if (json['success'] == true) {
logSafe("Employees assigned successfully");
return true;
} else {
logSafe("Failed to assign employees: ${json['message']}",
level: LogLevel.warning);
}
} catch (e, stack) {
logSafe("Exception during assignEmployeesToBucket API: $e",
level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
}
return false;
}
static Future<bool> createBucket({
required String name,
required String description,

View File

@ -11,6 +11,14 @@ class TeamMembersBottomSheet {
bool canEdit = false,
VoidCallback? onEdit,
}) {
// Ensure the owner is at the top of the list
final ownerId = bucket.createdBy.id;
members.sort((a, b) {
if (a.id == ownerId) return -1;
if (b.id == ownerId) return 1;
return 0;
});
showModalBottomSheet(
context: context,
isScrollControlled: true,
@ -42,7 +50,7 @@ class TeamMembersBottomSheet {
),
),
const SizedBox(height: 10),
// Title at top center
MyText.titleMedium(
'Bucket Details',
fontWeight: 700,
@ -50,7 +58,7 @@ class TeamMembersBottomSheet {
const SizedBox(height: 12),
// Header with title and optional edit button
// Header with title and edit
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
@ -71,7 +79,7 @@ class TeamMembersBottomSheet {
),
),
// Bucket info
// Info
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
@ -106,8 +114,6 @@ class TeamMembersBottomSheet {
),
],
),
// Can edit indicator
Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
@ -125,73 +131,7 @@ class TeamMembersBottomSheet {
],
),
),
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 SizedBox(height: 8),
const Divider(thickness: 1),
const SizedBox(height: 6),
MyText.labelLarge(
@ -205,7 +145,6 @@ class TeamMembersBottomSheet {
const SizedBox(height: 4),
// Members list
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
@ -226,6 +165,8 @@ class TeamMembersBottomSheet {
final member = members[index];
final firstName = member.firstName ?? '';
final lastName = member.lastName ?? '';
final isOwner =
member.id == bucket.createdBy.id;
return ListTile(
dense: true,
@ -235,9 +176,32 @@ class TeamMembersBottomSheet {
lastName: lastName,
size: 32,
),
title: MyText.bodyMedium(
'${firstName.isNotEmpty ? firstName : 'Unnamed'} ${lastName.isNotEmpty ? lastName : ''}',
fontWeight: 600,
title: Row(
children: [
Expanded(
child: MyText.bodyMedium(
'${firstName.isNotEmpty ? firstName : 'Unnamed'} ${lastName.isNotEmpty ? lastName : ''}',
fontWeight: 600,
),
),
if (isOwner)
Container(
margin:
const EdgeInsets.only(left: 6),
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius:
BorderRadius.circular(4),
),
child: MyText.labelSmall(
"Owner",
fontWeight: 600,
color: Colors.red,
),
),
],
),
subtitle: MyText.bodySmall(
member.jobRole ?? '',
@ -248,6 +212,7 @@ class TeamMembersBottomSheet {
),
),
),
const SizedBox(height: 8),
],
);

View File

@ -0,0 +1,300 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/controller/directory/manage_bucket_controller.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/model/directory/contact_bucket_list_model.dart';
import 'package:marco/model/employee_model.dart';
import 'package:marco/controller/directory/directory_controller.dart';
import 'package:collection/collection.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
class EditBucketBottomSheet {
static void show(BuildContext context, ContactBucket bucket,
List<EmployeeModel> allEmployees) {
final ManageBucketController controller = Get.find();
final nameController = TextEditingController(text: bucket.name);
final descController = TextEditingController(text: bucket.description);
final searchController = TextEditingController();
final selectedIds = RxSet<String>({...bucket.employeeIds});
final searchText = ''.obs;
InputDecoration _inputDecoration(String label) {
return InputDecoration(
labelText: label,
hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14),
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: const BorderSide(color: Colors.blueAccent, width: 1.5),
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
isDense: true,
);
}
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) {
return SingleChildScrollView(
padding: MediaQuery.of(context).viewInsets,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius:
const BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: const [
BoxShadow(
color: Colors.black12,
blurRadius: 12,
offset: Offset(0, -2),
),
],
),
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Center(
child: Container(
width: 40,
height: 5,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(10),
),
),
),
MySpacing.height(12),
Center(
child: MyText.titleMedium('Edit Bucket', fontWeight: 700),
),
MySpacing.height(24),
// Bucket Name
TextField(
controller: nameController,
decoration: _inputDecoration('Bucket Name'),
),
MySpacing.height(16),
// Description
TextField(
controller: descController,
maxLines: 2,
decoration: _inputDecoration('Description'),
),
MySpacing.height(20),
// Shared With
Align(
alignment: Alignment.centerLeft,
child: MyText.labelLarge('Shared With', fontWeight: 600),
),
MySpacing.height(8),
// Search
Obx(() => TextField(
controller: searchController,
onChanged: (value) =>
searchText.value = value.toLowerCase(),
decoration: InputDecoration(
hintText: 'Search employee...',
prefixIcon: const Icon(Icons.search, size: 20),
suffixIcon: searchText.value.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear, size: 18),
onPressed: () {
searchController.clear();
searchText.value = '';
},
)
: null,
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
borderSide:
BorderSide(color: Colors.blueAccent, width: 1.5),
),
),
)),
MySpacing.height(8),
// Employee list
Obx(() {
final filtered = allEmployees.where((emp) {
final fullName =
'${emp.firstName} ${emp.lastName}'.toLowerCase();
return fullName.contains(searchText.value);
}).toList();
return SizedBox(
height: 180,
child: ListView.builder(
itemCount: filtered.length,
itemBuilder: (context, index) {
final emp = filtered[index];
final fullName =
'${emp.firstName} ${emp.lastName}'.trim();
return Obx(() => Theme(
data: Theme.of(context).copyWith(
checkboxTheme: CheckboxThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
side: const BorderSide(
color: Colors.black, width: 2),
fillColor:
MaterialStateProperty.resolveWith<Color>(
(states) {
if (states
.contains(MaterialState.selected)) {
return Colors.blueAccent;
}
return Colors.transparent;
}),
checkColor:
MaterialStateProperty.all(Colors.white),
),
),
child: CheckboxListTile(
dense: true,
contentPadding: EdgeInsets.zero,
visualDensity:
const VisualDensity(vertical: -4),
controlAffinity:
ListTileControlAffinity.leading,
value: selectedIds.contains(emp.id),
onChanged: (val) {
if (val == true) {
selectedIds.add(emp.id);
} else {
selectedIds.remove(emp.id);
}
},
title: Text(
fullName.isNotEmpty ? fullName : 'Unnamed',
style: const TextStyle(fontSize: 13),
),
),
));
},
),
);
}),
MySpacing.height(24),
// Action Buttons
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () => Get.back(),
icon: const Icon(Icons.close, color: Colors.red),
label: MyText.bodyMedium("Cancel",
color: Colors.red, fontWeight: 600),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.red),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 12),
),
),
),
MySpacing.width(12),
Expanded(
child: ElevatedButton.icon(
onPressed: () async {
final newName = nameController.text.trim();
final newDesc = descController.text.trim();
final newEmployeeIds = selectedIds.toList()..sort();
final originalEmployeeIds = [...bucket.employeeIds]
..sort();
final nameChanged = newName != bucket.name;
final descChanged = newDesc != bucket.description;
final employeeChanged = !(ListEquality()
.equals(newEmployeeIds, originalEmployeeIds));
if (!nameChanged &&
!descChanged &&
!employeeChanged) {
showAppSnackbar(
title: "No Changes",
message:
"No changes were made to update the bucket.",
type: SnackbarType.warning,
);
return;
}
final success = await controller.updateBucket(
id: bucket.id,
name: newName,
description: newDesc,
employeeIds: newEmployeeIds,
originalEmployeeIds: originalEmployeeIds,
);
if (success) {
final directoryController =
Get.find<DirectoryController>();
await directoryController.fetchBuckets();
Navigator.of(context).pop();
}
},
icon: const Icon(Icons.check_circle_outline,
color: Colors.white),
label: MyText.bodyMedium("Save",
color: Colors.white, fontWeight: 600),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 12),
),
),
),
],
),
],
),
),
);
},
);
}
}

View File

@ -8,6 +8,7 @@ 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';
import 'package:marco/model/directory/edit_bucket_bottom_sheet.dart';
class ManageBucketsScreen extends StatefulWidget {
final PermissionController permissionController;
@ -93,199 +94,237 @@ class _ManageBucketsScreenState extends State<ManageBucketsScreen> {
),
),
),
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.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,
);
},
);
},
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,
@ -299,7 +338,6 @@ class _ManageBucketsScreenState extends State<ManageBucketsScreen> {
maxLines: 2,
textDirection: TextDirection.ltr,
)..layout(maxWidth: constraints.maxWidth);
final hasOverflow = tp.didExceedMaxLines;
return Column(
@ -338,18 +376,18 @@ class _ManageBucketsScreenState extends State<ManageBucketsScreen> {
);
},
),
),
],
],
),
),
),
],
),
);
},
);
}),
),
],
],
),
);
},
);
}),
),
],
),
),
);
}