Vaibhav_Task-#729 #53
@ -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';
|
||||
@ -103,7 +101,7 @@ class AddContactController extends GetxController {
|
||||
.whereType<String>()
|
||||
.toList();
|
||||
|
||||
// === Per-field Validation with Specific Messages ===
|
||||
// === Required validations only for name, organization, and bucket ===
|
||||
if (name.trim().isEmpty) {
|
||||
showAppSnackbar(
|
||||
title: "Missing Name",
|
||||
@ -122,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",
|
||||
@ -176,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];
|
||||
@ -207,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");
|
||||
|
70
lib/controller/directory/create_bucket_controller.dart
Normal file
70
lib/controller/directory/create_bucket_controller.dart
Normal file
@ -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<void> 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()}");
|
||||
}
|
||||
}
|
36
lib/controller/directory/manage_bucket_controller.dart
Normal file
36
lib/controller/directory/manage_bucket_controller.dart
Normal file
@ -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<EmployeeModel> allEmployees = <EmployeeModel>[].obs;
|
||||
RxBool isLoading = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchAllEmployees();
|
||||
}
|
||||
|
||||
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);
|
||||
} 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();
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
|
@ -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<bool> 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<Map<String, dynamic>?> getDirectoryNotes({
|
||||
int pageSize = 1000,
|
||||
int pageNumber = 1,
|
||||
|
90
lib/helpers/utils/contact_picker_helper.dart
Normal file
90
lib/helpers/utils/contact_picker_helper.dart
Normal file
@ -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<String?> 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<String>(
|
||||
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;
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
|
261
lib/helpers/widgets/team_members_bottom_sheet.dart
Normal file
261
lib/helpers/widgets/team_members_bottom_sheet.dart
Normal file
@ -0,0 +1,261 @@
|
||||
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,
|
||||
ContactBucket bucket,
|
||||
List<dynamic> members, {
|
||||
bool canEdit = false,
|
||||
VoidCallback? onEdit,
|
||||
}) {
|
||||
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.7,
|
||||
minChildSize: 0.5,
|
||||
maxChildSize: 0.95,
|
||||
builder: (context, scrollController) {
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
width: 36,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// 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: [
|
||||
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),
|
||||
child: members.isEmpty
|
||||
? Center(
|
||||
child: MyText.bodySmall(
|
||||
"No team members found.",
|
||||
fontWeight: 600,
|
||||
color: Colors.grey,
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
controller: scrollController,
|
||||
itemCount: members.length,
|
||||
separatorBuilder: (_, __) =>
|
||||
const SizedBox(height: 4),
|
||||
itemBuilder: (context, index) {
|
||||
final member = members[index];
|
||||
final firstName = member.firstName ?? '';
|
||||
final lastName = member.lastName ?? '';
|
||||
|
||||
return ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Avatar(
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
size: 32,
|
||||
),
|
||||
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),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
@ -25,7 +26,7 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
final addressController = TextEditingController();
|
||||
final descriptionController = TextEditingController();
|
||||
final tagTextController = TextEditingController();
|
||||
|
||||
final RxBool showAdvanced = false.obs;
|
||||
final RxList<TextEditingController> emailControllers =
|
||||
<TextEditingController>[].obs;
|
||||
final RxList<RxString> emailLabels = <RxString>[].obs;
|
||||
@ -184,8 +185,23 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
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<AddContactBottomSheet> {
|
||||
return "Enter valid phone number";
|
||||
}
|
||||
}
|
||||
|
||||
if (inputType == TextInputType.emailAddress &&
|
||||
!RegExp(r'^[\w.-]+@([\w-]+\.)+[\w-]{2,4}$')
|
||||
.hasMatch(trimmed)) {
|
||||
@ -506,188 +521,117 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
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(
|
||||
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<Color>((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),
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(
|
||||
top: 32,
|
||||
).add(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,
|
||||
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),
|
||||
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),
|
||||
],
|
||||
)
|
||||
: const SizedBox()),
|
||||
MySpacing.height(24),
|
||||
_buildActionButtons(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -695,4 +639,95 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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<Color>(
|
||||
(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),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
171
lib/model/directory/create_bucket_bottom_sheet.dart
Normal file
171
lib/model/directory/create_bucket_bottom_sheet.dart
Normal file
@ -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<CreateBucketBottomSheet> createState() => _CreateBucketBottomSheetState();
|
||||
}
|
||||
|
||||
class _CreateBucketBottomSheetState extends State<CreateBucketBottomSheet> {
|
||||
final BucketController _controller = Get.put(BucketController());
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
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<BucketController>(
|
||||
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<Color>(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<BucketController>();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -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,11 +11,21 @@ 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/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';
|
||||
|
||||
class DirectoryView extends StatelessWidget {
|
||||
class DirectoryView extends StatefulWidget {
|
||||
@override
|
||||
State<DirectoryView> createState() => _DirectoryViewState();
|
||||
}
|
||||
|
||||
class _DirectoryViewState extends State<DirectoryView> {
|
||||
final DirectoryController controller = Get.find();
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
final PermissionController permissionController =
|
||||
Get.put(PermissionController());
|
||||
|
||||
Future<void> _refreshDirectory() async {
|
||||
try {
|
||||
@ -25,23 +36,121 @@ class DirectoryView extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
void _handleCreateContact() async {
|
||||
await controller.fetchBuckets();
|
||||
|
||||
if (controller.contactBuckets.isEmpty) {
|
||||
final shouldCreate = await showModalBottomSheet<bool>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => _buildEmptyBucketPrompt(),
|
||||
);
|
||||
if (shouldCreate != true) return;
|
||||
|
||||
final created = await showModalBottomSheet<bool>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => const CreateBucketBottomSheet(),
|
||||
);
|
||||
if (created == true) {
|
||||
await controller.fetchBuckets();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Get.delete<BucketController>();
|
||||
|
||||
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(
|
||||
heroTag: 'createContact',
|
||||
backgroundColor: Colors.red,
|
||||
onPressed: () async {
|
||||
final result = await Get.bottomSheet(
|
||||
AddContactBottomSheet(),
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
);
|
||||
if (result == true) {
|
||||
controller.fetchContacts();
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.add, color: Colors.white),
|
||||
onPressed: _handleCreateContact,
|
||||
child: const Icon(Icons.person_add_alt_1, color: Colors.white),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
@ -155,28 +264,86 @@ class DirectoryView extends StatelessWidget {
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem<int>(
|
||||
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<PopupMenuEntry<int>> menuItems = [];
|
||||
|
||||
// Section: Actions (Always visible now)
|
||||
menuItems.add(
|
||||
const PopupMenuItem<int>(
|
||||
enabled: false,
|
||||
height: 30,
|
||||
child: Text(
|
||||
"Actions",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
menuItems.add(
|
||||
PopupMenuItem<int>(
|
||||
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<int>(
|
||||
enabled: false,
|
||||
height: 30,
|
||||
child: Text(
|
||||
"Preferences",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
menuItems.add(
|
||||
PopupMenuItem<int>(
|
||||
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;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -242,34 +409,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,
|
||||
@ -280,63 +475,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(', '),
|
||||
@ -348,7 +499,6 @@ class DirectoryView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.arrow_forward_ios,
|
||||
color: Colors.grey, size: 16),
|
||||
|
72
lib/view/directory/edit_bucket_model.dart
Normal file
72
lib/view/directory/edit_bucket_model.dart
Normal file
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
356
lib/view/directory/manage_bucket_screen.dart
Normal file
356
lib/view/directory/manage_bucket_screen.dart
Normal file
@ -0,0 +1,356 @@
|
||||
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<ManageBucketsScreen> createState() => _ManageBucketsScreenState();
|
||||
}
|
||||
|
||||
class _ManageBucketsScreenState extends State<ManageBucketsScreen> {
|
||||
final DirectoryController directoryController = Get.find();
|
||||
final ManageBucketController manageBucketController =
|
||||
Get.put(ManageBucketController());
|
||||
final ProjectController projectController = Get.find();
|
||||
|
||||
final Map<String, bool> _expandedMap = {};
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
String searchText = '';
|
||||
|
||||
void _clearSearch() {
|
||||
searchController.clear();
|
||||
setState(() {
|
||||
searchText = '';
|
||||
});
|
||||
}
|
||||
|
||||
@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<ProjectController>(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: 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();
|
||||
|
||||
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,
|
||||
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(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user