import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:on_field_work/controller/expense/add_expense_controller.dart'; import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; import 'package:on_field_work/model/expense/expense_type_model.dart'; import 'package:on_field_work/model/expense/payment_types_model.dart'; // import 'package:on_field_work/model/expense/employee_selector_bottom_sheet.dart'; import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart'; import 'package:on_field_work/helpers/utils/validators.dart'; import 'package:on_field_work/helpers/widgets/my_spacing.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/helpers/widgets/my_confirmation_dialog.dart'; import 'package:on_field_work/helpers/widgets/expense/expense_form_widgets.dart'; import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:on_field_work/model/employees/multiple_select_bottomsheet.dart'; /// Show bottom sheet wrapper Future showAddExpenseBottomSheet({ bool isEdit = false, Map? existingExpense, }) { return Get.bottomSheet( _AddExpenseBottomSheet( isEdit: isEdit, existingExpense: existingExpense, ), isScrollControlled: true, ); } /// Bottom sheet widget class _AddExpenseBottomSheet extends StatefulWidget { final bool isEdit; final Map? existingExpense; const _AddExpenseBottomSheet({ this.isEdit = false, this.existingExpense, }); @override State<_AddExpenseBottomSheet> createState() => _AddExpenseBottomSheetState(); } class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> with UIMixin { final AddExpenseController controller = Get.put(AddExpenseController()); final _formKey = GlobalKey(); final GlobalKey _projectDropdownKey = GlobalKey(); final GlobalKey _expenseTypeDropdownKey = GlobalKey(); final GlobalKey _paymentModeDropdownKey = GlobalKey(); /// Show employee list Future _showEmployeeList() async { final result = await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (_) => EmployeeSelectionBottomSheet( initiallySelected: controller.selectedPaidBy.value != null ? [controller.selectedPaidBy.value!] : [], multipleSelection: false, title: "Select Paid By", ), ); if (result == null) return; // result will be EmployeeModel or [EmployeeModel] if (result is EmployeeModel) { controller.setSelectedPaidBy(result); } else if (result is List && result.isNotEmpty) { controller.setSelectedPaidBy(result.first as EmployeeModel); } // cleanup try { controller.employeeSearchController.clear(); controller.employeeSearchResults.clear(); } catch (_) {} } /// Generic option list Future _showOptionList( List options, String Function(T) getLabel, ValueChanged onSelected, GlobalKey triggerKey, ) async { final RenderBox button = triggerKey.currentContext!.findRenderObject() as RenderBox; final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; final position = button.localToGlobal(Offset.zero, ancestor: overlay); final selected = await showMenu( context: context, position: RelativeRect.fromLTRB( position.dx, position.dy + button.size.height, overlay.size.width - position.dx - button.size.width, 0, ), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), items: options .map((opt) => PopupMenuItem( value: opt, child: Text(getLabel(opt)), )) .toList(), ); if (selected != null) onSelected(selected); } /// Validate required selections bool _validateSelections() { if (controller.selectedProject.value.isEmpty) { _showError("Please select a project"); return false; } if (controller.selectedExpenseType.value == null) { _showError("Please select an Expense Category"); return false; } if (controller.selectedPaymentMode.value == null) { _showError("Please select a payment mode"); return false; } if (controller.selectedPaidBy.value == null) { _showError("Please select a person who paid"); return false; } if (controller.attachments.isEmpty && controller.existingAttachments.isEmpty) { _showError("Please attach at least one document"); return false; } return true; } void _showError(String msg) { showAppSnackbar( title: "Error", message: msg, type: SnackbarType.error, ); } @override Widget build(BuildContext context) { return Obx( () => Form( key: _formKey, child: BaseBottomSheet( title: widget.isEdit ? "Edit Expense" : "Add Expense", isSubmitting: controller.isSubmitting.value, onCancel: Get.back, onSubmit: () { if (_formKey.currentState!.validate() && _validateSelections()) { controller.submitOrUpdateExpense(); } else { _showError("Please fill all required fields correctly"); } }, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildDropdownField( icon: Icons.work_outline, title: "Project", requiredField: true, value: controller.selectedProject.value.isEmpty ? "Select Project" : controller.selectedProject.value, onTap: () => _showOptionList( controller.globalProjects.toList(), (p) => p, (val) => controller.selectedProject.value = val, _projectDropdownKey, ), dropdownKey: _projectDropdownKey, ), _gap(), _buildDropdownField( icon: Icons.category_outlined, title: "Expense Category", requiredField: true, value: controller.selectedExpenseType.value?.name ?? "Select Expense Category", onTap: () => _showOptionList( controller.expenseTypes.toList(), (e) => e.name, (val) => controller.selectedExpenseType.value = val, _expenseTypeDropdownKey, ), dropdownKey: _expenseTypeDropdownKey, ), // Persons if required if (controller.selectedExpenseType.value?.noOfPersonsRequired == true) ...[ _gap(), _buildTextFieldSection( icon: Icons.people_outline, title: "No. of Persons", controller: controller.noOfPersonsController, hint: "Enter No. of Persons", keyboardType: TextInputType.number, validator: Validators.requiredField, ), ], _gap(), _buildTextFieldSection( icon: Icons.confirmation_number_outlined, title: "GST No.", controller: controller.gstController, hint: "Enter GST No.", ), _gap(), _buildDropdownField( icon: Icons.payment, title: "Payment Mode", requiredField: true, value: controller.selectedPaymentMode.value?.name ?? "Select Payment Mode", onTap: () => _showOptionList( controller.paymentModes.toList(), (p) => p.name, (val) => controller.selectedPaymentMode.value = val, _paymentModeDropdownKey, ), dropdownKey: _paymentModeDropdownKey, ), _gap(), _buildPaidBySection(), _gap(), _buildTextFieldSection( icon: Icons.currency_rupee, title: "Amount", controller: controller.amountController, hint: "Enter Amount", keyboardType: TextInputType.number, validator: (v) => Validators.isNumeric(v ?? "") ? null : "Enter valid amount", ), _gap(), _buildTextFieldSection( icon: Icons.store_mall_directory_outlined, title: "Supplier Name/Transporter Name/Other", controller: controller.supplierController, hint: "Enter Supplier Name/Transporter Name or Other", validator: Validators.nameValidator, ), _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(), _buildLocationField(), _gap(), _buildAttachmentsSection(), _gap(), _buildTextFieldSection( icon: Icons.description_outlined, title: "Description", controller: controller.descriptionController, hint: "Enter Description", maxLines: 3, validator: Validators.requiredField, ), ], ), ), ), ), ); } Widget _gap([double h = 16]) => MySpacing.height(h); Widget _buildDropdownField({ required IconData icon, required String title, required bool requiredField, required String value, required VoidCallback onTap, required GlobalKey dropdownKey, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SectionTitle(icon: icon, title: title, requiredField: requiredField), MySpacing.height(6), DropdownTile(key: dropdownKey, title: value, onTap: onTap), ], ); } Widget _buildTextFieldSection({ required IconData icon, required String title, required TextEditingController controller, String? hint, TextInputType? keyboardType, FormFieldValidator? validator, int maxLines = 1, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SectionTitle( icon: icon, title: title, requiredField: validator != null), MySpacing.height(6), CustomTextField( controller: controller, hint: hint ?? "", keyboardType: keyboardType ?? TextInputType.text, validator: validator, maxLines: maxLines, ), ], ); } Widget _buildPaidBySection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SectionTitle( icon: Icons.person_outline, title: "Paid By", requiredField: true), MySpacing.height(6), // Main tile: tap to choose mode + selection sheet GestureDetector( onTap: _showEmployeeList, child: TileContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( controller.selectedPaidBy.value?.name ?? "Select Paid By", style: TextStyle(fontSize: 15), overflow: TextOverflow.ellipsis, ), ), Icon(Icons.arrow_drop_down, size: 22), ], )), ), // small helper: long-press to quickly open multi-select directly (optional) const SizedBox(height: 6), ], ); } Widget _buildTransactionDateField() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SectionTitle( icon: Icons.calendar_today, title: "Transaction Date", requiredField: true), MySpacing.height(6), GestureDetector( onTap: () => controller.pickTransactionDate(context), child: AbsorbPointer( child: CustomTextField( controller: controller.transactionDateController, hint: "Select Transaction Date", validator: Validators.requiredField, ), ), ), ], ); } Widget _buildLocationField() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SectionTitle(icon: Icons.location_on_outlined, title: "Location"), MySpacing.height(6), TextFormField( controller: controller.locationController, decoration: InputDecoration( hintText: "Enter Location", filled: true, fillColor: Colors.grey.shade100, border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), suffixIcon: controller.isFetchingLocation.value ? const Padding( padding: EdgeInsets.all(12), child: SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ), ) : IconButton( icon: const Icon(Icons.my_location), tooltip: "Use Current Location", onPressed: controller.fetchCurrentLocation, ), ), ), ], ); } Widget _buildAttachmentsSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SectionTitle( icon: Icons.attach_file, title: "Attachments", requiredField: true, ), MySpacing.height(10), Obx(() { if (controller.isProcessingAttachment.value) { return Center( child: Column( children: [ CircularProgressIndicator( color: contentTheme.primary, ), const SizedBox(height: 8), Text( "Processing image, please wait...", style: TextStyle( fontSize: 14, color: contentTheme.primary, ), ), ], ), ); } return AttachmentsSection( attachments: controller.attachments, existingAttachments: controller.existingAttachments, onRemoveNew: controller.removeAttachment, controller: controller, onRemoveExisting: (item) async { await showDialog( context: context, barrierDismissible: false, builder: (_) => ConfirmDialog( title: "Remove Attachment", message: "Are you sure you want to remove this attachment?", confirmText: "Remove", icon: Icons.delete, confirmColor: Colors.redAccent, onConfirm: () async { final index = controller.existingAttachments.indexOf(item); if (index != -1) { controller.existingAttachments[index]['isActive'] = false; controller.existingAttachments.refresh(); } showAppSnackbar( title: 'Removed', message: 'Attachment has been removed.', type: SnackbarType.success, ); }, ), ); }, onAdd: controller.pickAttachments, ); }), ], ); } }