import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:marco/controller/finance/add_payment_request_controller.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.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/expense/expense_form_widgets.dart'; import 'package:marco/helpers/widgets/my_confirmation_dialog.dart'; Future showPaymentRequestBottomSheet({ bool isEdit = false, Map? existingData, VoidCallback? onUpdated, }) { return Get.bottomSheet( _PaymentRequestBottomSheet( isEdit: isEdit, existingData: existingData, onUpdated: onUpdated, ), isScrollControlled: true, ); } class _PaymentRequestBottomSheet extends StatefulWidget { final bool isEdit; final Map? existingData; final VoidCallback? onUpdated; const _PaymentRequestBottomSheet({ this.isEdit = false, this.existingData, this.onUpdated, }); @override State<_PaymentRequestBottomSheet> createState() => _PaymentRequestBottomSheetState(); } class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> with UIMixin { final controller = Get.put(AddPaymentRequestController()); final _formKey = GlobalKey(); final _projectDropdownKey = GlobalKey(); final _categoryDropdownKey = GlobalKey(); final _currencyDropdownKey = GlobalKey(); @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) async { if (widget.isEdit && widget.existingData != null) { final data = widget.existingData!; // Prefill text fields controller.titleController.text = data["title"] ?? ""; controller.amountController.text = data["amount"]?.toString() ?? ""; controller.descriptionController.text = data["description"] ?? ""; // Prefill due date if (data["dueDate"] != null && data["dueDate"].toString().isNotEmpty) { DateTime? dueDate = DateTime.tryParse(data["dueDate"].toString()); if (dueDate != null) { controller.selectedDueDate.value = dueDate; controller.dueDateController.text = DateFormat('dd MMM yyyy').format(dueDate); } } // Prefill dropdowns & toggles controller.selectedProject.value = { 'id': data["projectId"], 'name': data["projectName"], }; controller.selectedPayee.value = data["payee"] ?? ""; controller.isAdvancePayment.value = data["isAdvancePayment"] ?? false; // Categories & currencies everAll([controller.categories, controller.currencies], (_) { controller.selectedCategory.value = controller.categories .firstWhereOrNull((c) => c.id == data["expenseCategoryId"]); controller.selectedCurrency.value = controller.currencies .firstWhereOrNull((c) => c.id == data["currencyId"]); }); // Attachments final attachmentsData = data["attachments"]; if (attachmentsData != null && attachmentsData is List && attachmentsData.isNotEmpty) { final attachments = attachmentsData .whereType>() .map((a) => { "id": a["documentId"] ?? a["id"], "fileName": a["fileName"], "url": a["url"], "thumbUrl": a["thumbUrl"], "fileSize": a["fileSize"] ?? 0, "contentType": a["contentType"] ?? "", "isActive": true, }) .toList(); controller.existingAttachments.assignAll(attachments); } else { controller.existingAttachments.clear(); } } }); } @override Widget build(BuildContext context) { return Obx(() => Form( key: _formKey, child: BaseBottomSheet( title: widget.isEdit ? "Edit Payment Request" : "Create Payment Request", isSubmitting: controller.isSubmitting.value, onCancel: Get.back, submitText: "Save as Draft", onSubmit: () async { if (_formKey.currentState!.validate() && _validateSelections()) { bool success = false; if (widget.isEdit && widget.existingData != null) { final requestId = widget.existingData!['id']?.toString() ?? ''; if (requestId.isNotEmpty) { success = await controller.submitEditedPaymentRequest( requestId: requestId); } else { _showError("Invalid Payment Request ID"); return; } } else { success = await controller.submitPaymentRequest(); } if (success) { Get.back(); if (widget.onUpdated != null) widget.onUpdated!(); showAppSnackbar( title: "Success", message: widget.isEdit ? "Payment request updated successfully!" : "Payment request created successfully!", type: SnackbarType.success, ); } } }, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildDropdown( "Select Project", Icons.work_outline, controller.selectedProject.value?['name'] ?? "Select Project", controller.globalProjects, (p) => p['name'], controller.selectProject, key: _projectDropdownKey), _gap(), _buildDropdown( "Expense Category", Icons.category_outlined, controller.selectedCategory.value?.name ?? "Select Category", controller.categories, (c) => c.name, controller.selectCategory, key: _categoryDropdownKey), _gap(), _buildTextField( "Title", Icons.title_outlined, controller.titleController, hint: "Enter title", validator: Validators.requiredField), _gap(), _buildRadio("Is Advance Payment", Icons.attach_money_outlined, controller.isAdvancePayment, ["Yes", "No"]), _gap(), _buildDueDateField(), _gap(), _buildTextField("Amount", Icons.currency_rupee, controller.amountController, hint: "Enter Amount", keyboardType: TextInputType.number, validator: (v) => (v != null && v.isNotEmpty && double.tryParse(v) != null) ? null : "Enter valid amount"), _gap(), _buildPayeeAutocompleteField(), _gap(), _buildDropdown( "Currency", Icons.monetization_on_outlined, controller.selectedCurrency.value?.currencyName ?? "Select Currency", controller.currencies, (c) => c.currencyName, controller.selectCurrency, key: _currencyDropdownKey), _gap(), _buildTextField("Description", Icons.description_outlined, controller.descriptionController, hint: "Enter description", maxLines: 3, validator: Validators.requiredField), _gap(), _buildAttachmentsSection(), ], ), ), ), )); } 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, 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 _buildRadio( String title, IconData icon, RxBool controller, List labels) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(icon, size: 20), const SizedBox(width: 6), Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), ], ), MySpacing.height(6), Obx(() => Row( children: labels.asMap().entries.map((entry) { final i = entry.key; final label = entry.value; final value = i == 0; return Expanded( child: RadioListTile( contentPadding: EdgeInsets.zero, title: Text(label), value: value, groupValue: controller.value, activeColor: contentTheme.primary, onChanged: (val) => val != null ? controller.value = val : null, ), ); }).toList(), )), ], ); } Widget _buildDueDateField() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SectionTitle( icon: Icons.calendar_today, title: "Due To Date", requiredField: true), MySpacing.height(6), GestureDetector( onTap: () => controller.pickDueDate(context), child: AbsorbPointer( child: TextFormField( controller: controller.dueDateController, decoration: InputDecoration( hintText: "Select Due Date", filled: true, fillColor: Colors.grey.shade100, contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.grey.shade300), ), ), validator: (_) => controller.selectedDueDate.value == null ? "Please select a due date" : null, ), ), ), ], ); } Widget _buildPayeeAutocompleteField() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SectionTitle( icon: Icons.person_outline, title: "Payee", requiredField: true), const SizedBox(height: 6), Autocomplete( optionsBuilder: (textEditingValue) { final query = textEditingValue.text.toLowerCase(); return query.isEmpty ? const Iterable.empty() : controller.payees .where((p) => p.toLowerCase().contains(query)); }, displayStringForOption: (option) => option, fieldViewBuilder: (context, fieldController, focusNode, onFieldSubmitted) { // Avoid updating during build WidgetsBinding.instance.addPostFrameCallback((_) { if (fieldController.text != controller.selectedPayee.value) { fieldController.text = controller.selectedPayee.value; fieldController.selection = TextSelection.fromPosition( TextPosition(offset: fieldController.text.length)); } }); return TextFormField( controller: fieldController, focusNode: focusNode, decoration: InputDecoration( hintText: "Type or select payee", filled: true, fillColor: Colors.grey.shade100, contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.grey.shade300), ), ), validator: (v) => v == null || v.trim().isEmpty ? "Please enter payee" : null, onChanged: (val) => controller.selectedPayee.value = val, ); }, onSelected: (selection) => controller.selectedPayee.value = selection, optionsViewBuilder: (context, onSelected, options) => Material( color: Colors.white, elevation: 4, borderRadius: BorderRadius.circular(8), child: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 200, minWidth: 300), child: ListView.builder( padding: EdgeInsets.zero, itemCount: options.length, itemBuilder: (_, index) => InkWell( onTap: () => onSelected(options.elementAt(index)), child: Container( padding: const EdgeInsets.symmetric( vertical: 10, horizontal: 12), child: Text(options.elementAt(index), style: const TextStyle(fontSize: 14)), ), ), ), ), ), ), ], ); } 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, ); }), ], ); } 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; } if (key.currentContext == null) { final selected = await showDialog( context: context, builder: (_) => SimpleDialog( children: options .map((opt) => SimpleDialogOption( onPressed: () => Navigator.pop(context, opt), child: Text(getLabel(opt)), )) .toList(), ), ); if (selected != null) onSelected(selected); 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.selectedProject.value == null || controller.selectedProject.value!['id'].toString().isEmpty) { return _showError("Please select a project"); } if (controller.selectedCategory.value == null) { return _showError("Please select a category"); } if (controller.selectedPayee.value.isEmpty) { return _showError("Please select a payee"); } if (controller.selectedCurrency.value == null) { return _showError("Please select currency"); } return true; } bool _showError(String msg) { showAppSnackbar(title: "Error", message: msg, type: SnackbarType.error); return false; } }