import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/controller/expense/add_expense_controller.dart'; import 'package:marco/model/expense/expense_type_model.dart'; import 'package:marco/model/expense/payment_types_model.dart'; import 'package:marco/model/expense/employee_selector_bottom_sheet.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; import 'package:marco/helpers/utils/validators.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_confirmation_dialog.dart'; import 'package:marco/helpers/widgets/expense/expense_form_widgets.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> { 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 { await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (_) => ReusableEmployeeSelectorBottomSheet( searchController: controller.employeeSearchController, searchResults: controller.employeeSearchResults, isSearching: controller.isSearchingEmployees, onSearch: controller.searchEmployees, onSelect: (emp) => controller.selectedPaidBy.value = emp, ), ); controller.employeeSearchController.clear(); controller.employeeSearchResults.clear(); } /// 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 type"); 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 Type", requiredField: true, value: controller.selectedExpenseType.value?.name ?? "Select Expense Type", 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), GestureDetector( onTap: _showEmployeeList, child: TileContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( controller.selectedPaidBy.value == null ? "Select Paid By" : '${controller.selectedPaidBy.value?.firstName ?? ''} ${controller.selectedPaidBy.value?.lastName ?? ''}', style: const TextStyle(fontSize: 14), ), const Icon(Icons.arrow_drop_down, size: 22), ], ), ), ), ], ); } 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(6), AttachmentsSection( attachments: controller.attachments, existingAttachments: controller.existingAttachments, onRemoveNew: controller.removeAttachment, 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, ), ], ); } }