// create_expense_bottom_sheet.dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/expense/expense_form_widgets.dart'; import 'package:marco/helpers/utils/validators.dart'; import 'package:marco/controller/finance/payment_request_detail_controller.dart'; Future showCreateExpenseBottomSheet({required String statusId}) { return Get.bottomSheet( _CreateExpenseBottomSheet(statusId: statusId), isScrollControlled: true, ); } class _CreateExpenseBottomSheet extends StatefulWidget { final String statusId; const _CreateExpenseBottomSheet({required this.statusId, Key? key}) : super(key: key); @override State<_CreateExpenseBottomSheet> createState() => _CreateExpenseBottomSheetState(); } class _CreateExpenseBottomSheetState extends State<_CreateExpenseBottomSheet> { final controller = Get.put(PaymentRequestDetailController()); final _formKey = GlobalKey(); final TextEditingController commentController = TextEditingController(); final _paymentModeDropdownKey = GlobalKey(); @override Widget build(BuildContext context) { return Obx( () => Form( key: _formKey, child: BaseBottomSheet( title: "Create New Expense", isSubmitting: controller.isSubmitting.value, onCancel: Get.back, onSubmit: () async { if (_formKey.currentState!.validate() && _validateSelections()) { final success = await controller.submitExpense( statusId: widget.statusId, comment: commentController.text.trim(), ); if (success) { Get.back(); showAppSnackbar( title: "Success", message: "Expense created successfully!", type: SnackbarType.success, ); } } ; }, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildDropdown( "Payment Mode*", Icons.payment_outlined, controller.selectedPaymentMode.value?.name ?? "Select Mode", controller.paymentModes, (p) => p.name, controller.selectPaymentMode, key: _paymentModeDropdownKey, ), _gap(), _buildTextField( "GST Number", Icons.receipt_outlined, controller.gstNumberController, hint: "Enter GST Number", validator: null, ), _gap(), _buildTextField( "Location*", Icons.location_on_outlined, controller.locationController, hint: "Enter location", validator: Validators.requiredField, keyboardType: TextInputType.text, suffixIcon: IconButton( icon: const Icon(Icons.my_location_outlined), onPressed: () async { await controller.fetchCurrentLocation(); }, ), ), _gap(), _buildAttachmentField(), _gap(), _buildTextField( "Comment", Icons.comment_outlined, commentController, hint: "Enter a comment (optional)", validator: null, ), _gap(), ], ), ), ), ), ); } Widget _buildDropdown(String title, IconData icon, String value, List options, String Function(T) getLabel, ValueChanged onSelected, {required GlobalKey key}) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SectionTitle(icon: icon, title: title, requiredField: true), MySpacing.height(6), DropdownTile( key: key, title: value, onTap: () => _showOptionList(options, getLabel, onSelected, key), ), ], ); } Widget _buildTextField( String title, IconData icon, TextEditingController controller, { String? hint, FormFieldValidator? validator, TextInputType? keyboardType, Widget? suffixIcon, // add this }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SectionTitle( icon: icon, title: title, requiredField: validator != null), MySpacing.height(6), CustomTextField( controller: controller, hint: hint ?? "", validator: validator, keyboardType: keyboardType ?? TextInputType.text, suffixIcon: suffixIcon, ), ], ); } Widget _buildAttachmentField() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SectionTitle( icon: Icons.attach_file, title: "Upload Bill*", requiredField: true), MySpacing.height(6), Obx(() { if (controller.isProcessingAttachment.value) { return Center( child: Column( children: const [ CircularProgressIndicator(), SizedBox(height: 8), Text("Processing file, please wait..."), ], ), ); } return AttachmentsSection( attachments: controller.attachments, existingAttachments: controller.existingAttachments, onRemoveNew: controller.removeAttachment, controller: controller, onAdd: controller.pickAttachments, ); }), ], ); } Widget _gap([double h = 16]) => MySpacing.height(h); Future _showOptionList(List options, String Function(T) getLabel, ValueChanged onSelected, GlobalKey key) async { if (options.isEmpty) { _showError("No options available"); return; } 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); 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); } bool _validateSelections() { if (controller.selectedPaymentMode.value == null) { return _showError("Please select a payment mode"); } if (controller.locationController.text.trim().isEmpty) { return _showError("Please enter location"); } if (controller.attachments.isEmpty) { return _showError("Please upload bill"); } return true; } bool _showError(String msg) { showAppSnackbar(title: "Error", message: msg, type: SnackbarType.error); return false; } }