import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.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'; import 'package:marco/view/project/create_project_bottom_sheet.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(); @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: _handleSubmit, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildCreateProjectButton(), _buildProjectDropdown(), _gap(), _buildExpenseTypeDropdown(), if (controller.selectedExpenseType.value?.noOfPersonsRequired == true) ...[ _gap(), _buildNumberField( icon: Icons.people_outline, title: "No. of Persons", controller: controller.noOfPersonsController, hint: "Enter No. of Persons", validator: Validators.requiredField, ), ], _gap(), _buildPaymentModeDropdown(), _gap(), _buildPaidBySection(), _gap(), _buildAmountField(), _gap(), _buildSupplierField(), _gap(), _buildTransactionDateField(), _gap(), _buildTransactionIdField(), _gap(), _buildLocationField(), _gap(), _buildAttachmentsSection(), _gap(), _buildDescriptionField(), ], ), ), ), ), ); } /// 🟦 UI SECTION BUILDERS Widget _buildCreateProjectButton() { return Align( alignment: Alignment.centerRight, child: TextButton.icon( onPressed: () async { await Get.bottomSheet(const CreateProjectBottomSheet(), isScrollControlled: true); await controller.fetchGlobalProjects(); }, icon: const Icon(Icons.add, color: Colors.blue), label: const Text( "Create Project", style: TextStyle(color: Colors.blue, fontWeight: FontWeight.w600), ), ), ); } Widget _buildProjectDropdown() { return _buildDropdownField( icon: Icons.work_outline, title: "Project", requiredField: true, value: controller.selectedProject.value.isEmpty ? "Select Project" : controller.selectedProject.value, onTap: _showProjectSelector, dropdownKey: _projectDropdownKey, ); } Widget _buildExpenseTypeDropdown() { return _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, ); } Widget _buildPaymentModeDropdown() { return _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, ); } Widget _buildPaidBySection() { return _buildTileSelector( icon: Icons.person_outline, title: "Paid By", required: true, displayText: controller.selectedPaidBy.value == null ? "Select Paid By" : '${controller.selectedPaidBy.value?.firstName ?? ''} ${controller.selectedPaidBy.value?.lastName ?? ''}', onTap: _showEmployeeList, ); } Widget _buildAmountField() => _buildNumberField( icon: Icons.currency_rupee, title: "Amount", controller: controller.amountController, hint: "Enter Amount", validator: (v) => Validators.isNumeric(v ?? "") ? null : "Enter valid amount", ); Widget _buildSupplierField() => _buildTextField( 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, ); Widget _buildTransactionIdField() { final paymentMode = controller.selectedPaymentMode.value?.name.toLowerCase() ?? ''; final isRequired = paymentMode.isNotEmpty && paymentMode != 'cash' && paymentMode != 'cheque'; return _buildTextField( icon: Icons.confirmation_number_outlined, title: "Transaction ID", 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; }, requiredField: isRequired, ); } Widget _buildTransactionDateField() { return Obx(() => _buildTileSelector( icon: Icons.calendar_today, title: "Transaction Date", required: true, displayText: controller.selectedTransactionDate.value == null ? "Select Transaction Date" : DateFormat('dd MMM yyyy') .format(controller.selectedTransactionDate.value!), onTap: () => controller.pickTransactionDate(context), )); } 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: _confirmRemoveAttachment, onAdd: controller.pickAttachments, ), ], ); } Widget _buildDescriptionField() => _buildTextField( icon: Icons.description_outlined, title: "Description", controller: controller.descriptionController, hint: "Enter Description", maxLines: 3, validator: Validators.requiredField, ); /// 🟩 COMMON HELPERS 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 _buildTextField({ required IconData icon, required String title, required TextEditingController controller, String? hint, FormFieldValidator? validator, bool requiredField = true, int maxLines = 1, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SectionTitle(icon: icon, title: title, requiredField: requiredField), MySpacing.height(6), CustomTextField( controller: controller, hint: hint ?? "", validator: validator, maxLines: maxLines, ), ], ); } Widget _buildNumberField({ required IconData icon, required String title, required TextEditingController controller, String? hint, FormFieldValidator? validator, }) { return _buildTextField( icon: icon, title: title, controller: controller, hint: hint, validator: validator, ); } Widget _buildTileSelector({ required IconData icon, required String title, required String displayText, required VoidCallback onTap, bool required = false, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SectionTitle(icon: icon, title: title, requiredField: required), MySpacing.height(6), GestureDetector( onTap: onTap, child: TileContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(displayText, style: const TextStyle(fontSize: 14)), const Icon(Icons.arrow_drop_down, size: 22), ], ), ), ), ], ); } /// 🧰 LOGIC HELPERS Future _showProjectSelector() async { final sortedProjects = controller.globalProjects.toList() ..sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase())); const specialOption = 'Create New Project'; final displayList = [...sortedProjects, specialOption]; final selected = await showMenu( context: context, position: _getPopupMenuPosition(_projectDropdownKey), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), items: displayList.map((opt) { final isSpecial = opt == specialOption; return PopupMenuItem( value: opt, child: isSpecial ? Row( children: const [ Icon(Icons.add, color: Colors.blue), SizedBox(width: 8), Text( specialOption, style: TextStyle( fontWeight: FontWeight.w600, color: Colors.blue, ), ), ], ) : Text( opt, style: const TextStyle( fontWeight: FontWeight.normal, color: Colors.black, ), ), ); }).toList(), ); if (selected == null) return; if (selected == specialOption) { controller.selectedProject.value = specialOption; await Get.bottomSheet(const CreateProjectBottomSheet(), isScrollControlled: true); await controller.fetchGlobalProjects(); controller.selectedProject.value = ""; } else { controller.selectedProject.value = selected; } } 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(); } Future _confirmRemoveAttachment(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, ); }, ), ); } Future _showOptionList( List options, String Function(T) getLabel, ValueChanged onSelected, GlobalKey triggerKey, ) async { final selected = await showMenu( context: context, position: _getPopupMenuPosition(triggerKey), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), items: options .map((opt) => PopupMenuItem( value: opt, child: Text(getLabel(opt)), )) .toList(), ); if (selected != null) onSelected(selected); } RelativeRect _getPopupMenuPosition(GlobalKey key) { final RenderBox button = key.currentContext!.findRenderObject() as RenderBox; final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; final position = button.localToGlobal(Offset.zero, ancestor: overlay); return RelativeRect.fromLTRB( position.dx, position.dy + button.size.height, overlay.size.width - position.dx - button.size.width, 0, ); } bool _validateSelections() { if (controller.selectedProject.value.isEmpty) { return _error("Please select a project"); } if (controller.selectedExpenseType.value == null) { return _error("Please select an expense type"); } if (controller.selectedPaymentMode.value == null) { return _error("Please select a payment mode"); } if (controller.selectedPaidBy.value == null) { return _error("Please select a person who paid"); } if (controller.attachments.isEmpty && controller.existingAttachments.isEmpty) { return _error("Please attach at least one document"); } return true; } bool _error(String msg) { showAppSnackbar(title: "Error", message: msg, type: SnackbarType.error); return false; } void _handleSubmit() { if (_formKey.currentState!.validate() && _validateSelections()) { controller.submitOrUpdateExpense(); } else { _error("Please fill all required fields correctly"); } } }