made chnages in add contach for assignment bucket

This commit is contained in:
Vaibhav Surve 2025-10-01 12:14:37 +05:30
parent a18c4dad45
commit 2b8196b216
5 changed files with 236 additions and 69 deletions

View File

@ -10,7 +10,7 @@ class AddContactController extends GetxController {
final RxList<String> tags = <String>[].obs;
final RxString selectedCategory = ''.obs;
final RxString selectedBucket = ''.obs;
final RxList<String> selectedBuckets = <String>[].obs;
final RxString selectedProject = ''.obs;
final RxList<String> enteredTags = <String>[].obs;
@ -50,7 +50,7 @@ class AddContactController extends GetxController {
void resetForm() {
selectedCategory.value = '';
selectedProject.value = '';
selectedBucket.value = '';
selectedBuckets.clear();
enteredTags.clear();
filteredSuggestions.clear();
filteredOrgSuggestions.clear();
@ -100,7 +100,21 @@ class AddContactController extends GetxController {
isSubmitting.value = true;
final categoryId = categoriesMap[selectedCategory.value];
final bucketId = bucketsMap[selectedBucket.value];
final bucketIds = selectedBuckets
.map((name) => bucketsMap[name])
.whereType<String>()
.toList();
if (bucketIds.isEmpty) {
showAppSnackbar(
title: "Missing Buckets",
message: "Please select at least one bucket.",
type: SnackbarType.warning,
);
isSubmitting.value = false;
return;
}
final projectIds = selectedProjects
.map((name) => projectsMap[name])
.whereType<String>()
@ -126,10 +140,10 @@ class AddContactController extends GetxController {
return;
}
if (selectedBucket.value.trim().isEmpty || bucketId == null) {
if (selectedBuckets.isEmpty) {
showAppSnackbar(
title: "Missing Bucket",
message: "Please select a bucket.",
message: "Please select at least one bucket.",
type: SnackbarType.warning,
);
isSubmitting.value = false;
@ -151,7 +165,7 @@ class AddContactController extends GetxController {
if (selectedCategory.value.isNotEmpty && categoryId != null)
"contactCategoryId": categoryId,
if (projectIds.isNotEmpty) "projectIds": projectIds,
"bucketIds": [bucketId],
"bucketIds": bucketIds,
if (enteredTags.isNotEmpty) "tags": tagObjects,
if (emails.isNotEmpty) "contactEmails": emails,
if (phones.isNotEmpty) "contactPhones": phones,

View File

@ -73,7 +73,8 @@ class AddEmployeeController extends MyController {
controller: TextEditingController(),
);
logSafe('Fields initialized for first_name, phone_number, last_name, email.');
logSafe(
'Fields initialized for first_name, phone_number, last_name, email.');
}
// Prefill fields in edit mode
@ -87,7 +88,8 @@ class AddEmployeeController extends MyController {
editingEmployeeData?['phone_number'] ?? '';
selectedGender = editingEmployeeData?['gender'] != null
? Gender.values.firstWhereOrNull((g) => g.name == editingEmployeeData!['gender'])
? Gender.values
.firstWhereOrNull((g) => g.name == editingEmployeeData!['gender'])
: null;
basicValidator.getController('email')?.text =
@ -121,12 +123,24 @@ class AddEmployeeController extends MyController {
if (result != null) {
roles = List<Map<String, dynamic>>.from(result);
logSafe('Roles fetched successfully.');
// If editing, and role already selected, update the role controller text here
if (editingEmployeeData != null && selectedRoleId != null) {
final selectedRole = roles.firstWhereOrNull(
(r) => r['id'] == selectedRoleId,
);
if (selectedRole != null) {
update();
}
}
update();
} else {
logSafe('Failed to fetch roles: null result', level: LogLevel.error);
}
} catch (e, st) {
logSafe('Error fetching roles', level: LogLevel.error, error: e, stackTrace: st);
logSafe('Error fetching roles',
level: LogLevel.error, error: e, stackTrace: st);
}
}
@ -156,7 +170,8 @@ class AddEmployeeController extends MyController {
final firstName = basicValidator.getController('first_name')?.text.trim();
final lastName = basicValidator.getController('last_name')?.text.trim();
final phoneNumber = basicValidator.getController('phone_number')?.text.trim();
final phoneNumber =
basicValidator.getController('phone_number')?.text.trim();
try {
// sanitize orgId before sending
@ -216,7 +231,8 @@ class AddEmployeeController extends MyController {
showAppSnackbar(
title: 'Permission Required',
message: 'Please allow Contacts permission from settings to pick a contact.',
message:
'Please allow Contacts permission from settings to pick a contact.',
type: SnackbarType.warning,
);
return false;

View File

@ -95,7 +95,9 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
final name = controller.bucketsMap.entries
.firstWhereOrNull((e) => e.value == bucketId)
?.key;
if (name != null) controller.selectedBucket.value = name;
if (name != null && !controller.selectedBuckets.contains(name)) {
controller.selectedBuckets.add(name);
}
}
}
});
@ -363,10 +365,127 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
);
}
Widget _bucketMultiSelectField() {
return _multiSelectField(
items: controller.buckets
.map((name) => FilterItem(id: name, name: name))
.toList(),
fallback: "Choose Buckets",
selectedValues: controller.selectedBuckets,
);
}
Widget _multiSelectField({
required List<FilterItem> items,
required String fallback,
required RxList<String> selectedValues,
}) {
if (items.isEmpty) return const SizedBox.shrink();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Obx(() {
final selectedNames = items
.where((f) => selectedValues.contains(f.id))
.map((f) => f.name)
.join(", ");
final displayText =
selectedNames.isNotEmpty ? selectedNames : fallback;
return Builder(
builder: (context) {
return GestureDetector(
onTap: () async {
final RenderBox button =
context.findRenderObject() as RenderBox;
final RenderBox overlay = Overlay.of(context)
.context
.findRenderObject() as RenderBox;
final position = button.localToGlobal(Offset.zero);
await showMenu(
context: context,
position: RelativeRect.fromLTRB(
position.dx,
position.dy + button.size.height,
overlay.size.width - position.dx - button.size.width,
0,
),
items: items.map((f) {
return PopupMenuItem<String>(
enabled: false,
child: StatefulBuilder(
builder: (context, setState) {
final isChecked = selectedValues.contains(f.id);
return CheckboxListTile(
dense: true,
value: isChecked,
contentPadding: EdgeInsets.zero,
controlAffinity: ListTileControlAffinity.leading,
title: MyText(f.name),
checkColor: Colors.white,
side: const BorderSide(
color: Colors.black, width: 1.5),
fillColor:
MaterialStateProperty.resolveWith<Color>(
(states) {
if (states.contains(MaterialState.selected)) {
return Colors.indigo;
}
return Colors.white;
},
),
onChanged: (val) {
if (val == true) {
selectedValues.add(f.id);
} else {
selectedValues.remove(f.id);
}
setState(() {});
},
);
},
),
);
}).toList(),
);
},
child: Container(
padding: MySpacing.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: MyText(
displayText,
style: const TextStyle(color: Colors.black87),
overflow: TextOverflow.ellipsis,
),
),
const Icon(Icons.arrow_drop_down, color: Colors.grey),
],
),
),
);
},
);
}),
MySpacing.height(16),
],
);
}
void _handleSubmit() {
bool valid = formKey.currentState?.validate() ?? false;
if (controller.selectedBucket.value.isEmpty) {
if (controller.selectedBuckets.isEmpty) {
bucketError.value = "Bucket is required";
valid = false;
} else {
@ -430,29 +549,14 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
MySpacing.height(16),
_textField("Organization", orgCtrl, required: true),
MySpacing.height(16),
_labelWithStar("Bucket", required: true),
_labelWithStar("Buckets", required: true),
MySpacing.height(8),
Stack(
children: [
_popupSelector(controller.selectedBucket, controller.buckets,
"Choose Bucket"),
Positioned(
left: 0,
right: 0,
top: 56,
child: Obx(() => bucketError.value.isEmpty
? const SizedBox.shrink()
: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(bucketError.value,
style: const TextStyle(
color: Colors.red, fontSize: 12)),
)),
),
_bucketMultiSelectField(),
],
),
MySpacing.height(24),
MySpacing.height(12),
Obx(() => GestureDetector(
onTap: () => showAdvanced.toggle(),
child: Row(
@ -562,3 +666,9 @@ class _AddContactBottomSheetState extends State<AddContactBottomSheet> {
});
}
}
class FilterItem {
final String id;
final String name;
FilterItem({required this.id, required this.name});
}

View File

@ -37,36 +37,39 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
void initState() {
super.initState();
_controller = Get.put(
AddEmployeeController(),
// Unique tag to avoid clashes, but stable for this widget instance
tag: UniqueKey().toString(),
);
_joiningDateController = TextEditingController();
_genderController = TextEditingController();
_roleController = TextEditingController();
_orgFieldController = TextEditingController(text: '');
_joiningDateController = TextEditingController(text: '');
_genderController = TextEditingController(text: '');
_roleController = TextEditingController(text: '');
_controller = Get.put(AddEmployeeController(), tag: UniqueKey().toString());
// Prefill when editing
if (widget.employeeData != null) {
_controller.editingEmployeeData = widget.employeeData;
_controller.prefillFields();
// 👇 joining date & gender already handled in your code
if (_controller.joiningDate != null) {
_joiningDateController.text =
DateFormat('dd MMM yyyy').format(_controller.joiningDate!);
}
if (_controller.selectedGender != null) {
_genderController.text =
_controller.selectedGender!.name.capitalizeFirst ?? '';
}
final roleName = _controller.roles.firstWhereOrNull(
(r) => r['id'] == _controller.selectedRoleId)?['name'] ??
'';
_roleController.text = roleName;
} else {}
// Important part: fetch roles, then set roleController
_controller.fetchRoles().then((_) {
if (_controller.selectedRoleId != null) {
final roleName = _controller.roles.firstWhereOrNull(
(r) => r['id'] == _controller.selectedRoleId,
)?['name'];
if (roleName != null) {
_roleController.text = roleName;
}
}
});
} else {
_controller.fetchRoles();
}
}
@override
@ -422,7 +425,7 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
context: context,
initialDate: _controller.joiningDate ?? DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2100),
lastDate: DateTime.now(),
);
if (picked != null) {
@ -435,6 +438,15 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
Future<void> _handleSubmit() async {
final isValid =
_controller.basicValidator.formKey.currentState?.validate() ?? false;
if (_controller.joiningDate != null &&
_controller.joiningDate!.isAfter(DateTime.now())) {
showAppSnackbar(
title: 'Invalid Date',
message: 'Joining Date cannot be in the future.',
type: SnackbarType.warning,
);
return;
}
if (!isValid ||
_controller.joiningDate == null ||

View File

@ -204,14 +204,6 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
],
_gap(),
_buildTextFieldSection(
icon: Icons.confirmation_number_outlined,
title: "GST No.",
controller: controller.gstController,
hint: "Enter GST No.",
),
_gap(),
_buildDropdownField<PaymentModeModel>(
icon: Icons.payment,
title: "Payment Mode",
@ -252,20 +244,11 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
),
_gap(),
_buildTextFieldSection(
icon: Icons.confirmation_number_outlined,
title: "Transaction ID",
controller: controller.transactionIdController,
hint: "Enter Transaction ID",
validator: (v) => (v != null && v.isNotEmpty)
? Validators.transactionIdValidator(v)
: null,
),
_gap(),
_buildTransactionDateField(),
_gap(),
_buildTransactionIdField(),
_gap(),
_buildLocationField(),
_gap(),
@ -288,6 +271,39 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
);
}
Widget _buildTransactionIdField() {
final paymentMode =
controller.selectedPaymentMode.value?.name.toLowerCase() ?? '';
final isRequired = paymentMode.isNotEmpty &&
paymentMode != 'cash' &&
paymentMode != 'cheque';
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionTitle(
icon: Icons.confirmation_number_outlined,
title: "Transaction ID",
requiredField: isRequired,
),
MySpacing.height(6),
CustomTextField(
controller: controller.transactionIdController,
hint: "Enter Transaction ID",
validator: (v) {
if (isRequired) {
if (v == null || v.isEmpty) {
return "Transaction ID is required for this payment mode";
}
return Validators.transactionIdValidator(v);
}
return null;
},
),
],
);
}
Widget _gap([double h = 16]) => MySpacing.height(h);
Widget _buildDropdownField<T>({
@ -326,8 +342,7 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
CustomTextField(
controller: controller,
hint: hint ?? "",
keyboardType:
keyboardType ?? TextInputType.text,
keyboardType: keyboardType ?? TextInputType.text,
validator: validator,
maxLines: maxLines,
),