// ignore_for_file: library_private_types_in_public_api, unused_result import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:hugeicons/hugeicons.dart'; import 'package:nb_utils/nb_utils.dart'; import '../../Screens/cash and bank/bank account/provider/bank_account_provider.dart'; import '../../constant.dart'; import '../../generated/l10n.dart' as lang; import 'model/payment_transaction_model.dart'; class PaymentEntry { String? type; final TextEditingController amountController = TextEditingController(); final TextEditingController chequeNumberController = TextEditingController(); final GlobalKey typeKey = GlobalKey(); final GlobalKey amountKey = GlobalKey(); PaymentEntry({this.type}); void dispose() { amountController.dispose(); chequeNumberController.dispose(); } Map toJson() { return { 'type': type, 'amount': num.tryParse(amountController.text) ?? 0, 'cheque_number': chequeNumberController.text, }; } } class MultiPaymentWidget extends ConsumerStatefulWidget { final TextEditingController totalAmountController; final bool showChequeOption; final bool showWalletOption; final bool hideAddButton; final bool disableDropdown; final VoidCallback? onPaymentListChanged; final List? initialTransactions; const MultiPaymentWidget({ super.key, required this.totalAmountController, this.showChequeOption = false, this.showWalletOption = false, this.hideAddButton = false, this.disableDropdown = false, this.onPaymentListChanged, this.initialTransactions, }); @override MultiPaymentWidgetState createState() => MultiPaymentWidgetState(); } class MultiPaymentWidgetState extends ConsumerState { List _paymentEntries = []; bool _isSyncing = false; /// Public method to get payment entries /// This can be accessed via a GlobalKey List getPaymentEntries() { return _paymentEntries; } @override void initState() { super.initState(); _initializePaymentEntries(); // This listener syncs from TOTAL -> PAYMENT (if 1 row) widget.totalAmountController.addListener(_onTotalAmountSync); // This listener syncs from PAYMENT -> TOTAL (for all cases) _paymentEntries[0].amountController.addListener(_calculateTotalsFromPayments); } void _initializePaymentEntries() { if (widget.initialTransactions != null && widget.initialTransactions!.isNotEmpty) { for (var trans in widget.initialTransactions!) { String type = 'Cash'; if (trans.transactionType?.toLowerCase().contains('cheque') ?? false) { type = 'Cheque'; } else if (trans.paymentTypeId != null) { type = trans.paymentTypeId.toString(); } else if (trans.transactionType?.toLowerCase().contains('cash') ?? false) { type = 'Cash'; } PaymentEntry entry = PaymentEntry(type: type); entry.amountController.text = trans.amount?.toString() ?? '0'; entry.amountController.addListener(_calculateTotalsFromPayments); if (type == 'Cheque') { entry.chequeNumberController.text = trans.meta?.chequeNumber ?? ''; } _paymentEntries.add(entry); } } else { _paymentEntries = [PaymentEntry(type: 'Cash')]; _paymentEntries[0].amountController.addListener(_calculateTotalsFromPayments); } } @override void dispose() { widget.totalAmountController.removeListener(_onTotalAmountSync); for (var entry in _paymentEntries) { entry.amountController.removeListener(_calculateTotalsFromPayments); entry.dispose(); } super.dispose(); } // Listener for the main "Total Amount" field void _onTotalAmountSync() { if (_isSyncing || _paymentEntries.length != 1) return; _isSyncing = true; final totalText = widget.totalAmountController.text; if (_paymentEntries[0].amountController.text != totalText) { _paymentEntries[0].amountController.text = totalText; } setState(() {}); _isSyncing = false; } // Listener for all payment amount fields void _calculateTotalsFromPayments() { if (_isSyncing) return; _isSyncing = true; double total = 0.0; for (var entry in _paymentEntries) { total += double.tryParse(entry.amountController.text) ?? 0.0; } setState(() { if (mounted) { // Only update parent if value is different to avoid infinite loop if (widget.totalAmountController.text != total.toStringAsFixed(2)) { widget.totalAmountController.text = total.toStringAsFixed(2); } } }); _isSyncing = false; } // Add listener when adding a new row void _addPaymentRow() { final newEntry = PaymentEntry(); newEntry.amountController.addListener(_calculateTotalsFromPayments); setState(() { _paymentEntries.add(newEntry); }); widget.onPaymentListChanged?.call(); _calculateTotalsFromPayments(); } // Remove listener when removing a row void _removePaymentRow(int index) { if (_paymentEntries.length > 1) { final entry = _paymentEntries[index]; entry.amountController.removeListener(_calculateTotalsFromPayments); entry.dispose(); setState(() { _paymentEntries.removeAt(index); }); widget.onPaymentListChanged?.call(); _calculateTotalsFromPayments(); } else { EasyLoading.showError('At least one payment method is required'); } } @override Widget build(BuildContext context) { final _theme = Theme.of(context); final _lang = lang.S.of(context); final bankListAsync = ref.watch(bankListProvider); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ///________PaymentType__________________________________ Text( lang.S.of(context).paymentTypes, style: _theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 10), // Build dynamic payment rows bankListAsync.when( data: (bankData) { List> paymentTypeItems = [ DropdownMenuItem( value: 'Cash', child: Text(lang.S.of(context).cash), ), if (widget.showWalletOption) const DropdownMenuItem( value: 'wallet', child: Text("Wallet"), ), if (widget.showChequeOption) const DropdownMenuItem( value: 'Cheque', child: Text("Cheque"), ), ...(bankData.data?.map((bank) => DropdownMenuItem( value: bank.id.toString(), child: Text(bank.name ?? 'Unknown Bank'), )) ?? []), ]; return Column( children: [ ..._paymentEntries.asMap().entries.map((entry) { int index = entry.key; PaymentEntry payment = entry.value; return _buildPaymentRow(payment, index, paymentTypeItems, readonly: widget.hideAddButton, disableDropdown: widget.disableDropdown); }), if (!widget.hideAddButton) const SizedBox(height: 4), // "Add Payment" Button if (!widget.hideAddButton) SizedBox( width: double.infinity, child: TextButton.icon( icon: const Icon(Icons.add), label: Text(_lang.addPayment), onPressed: _addPaymentRow, style: TextButton.styleFrom( foregroundColor: kMainColor, side: const BorderSide(color: kMainColor), ), ), ), ], ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (err, stack) => Text('Error loading banks: $err'), ), ], ); } Widget _buildPaymentRow(PaymentEntry payment, int index, List> paymentTypeItems, {bool readonly = false, bool disableDropdown = false}) { return Column( children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Payment Type Dropdown Expanded( flex: 3, child: DropdownButtonFormField( isExpanded: true, icon: Icon( Icons.keyboard_arrow_down, color: kGreyTextColor, ), key: payment.typeKey, value: payment.type, hint: Text(lang.S.of(context).selectType), items: paymentTypeItems, onChanged: disableDropdown ? null : (value) { setState(() { payment.type = value; }); }, validator: (value) { if (value == null) { return 'Required'; } return null; }, ), ), const SizedBox(width: 10), // Amount Field Expanded( flex: 2, child: TextFormField( key: payment.amountKey, readOnly: readonly, controller: payment.amountController, decoration: kInputDecoration.copyWith(labelText: lang.S.of(context).amount, hintText: 'Ex: 10'), keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))], validator: (value) { if (value.isEmptyOrNull) { return 'Required'; } if ((double.tryParse(value!) ?? 0) < 0) { return 'Invalid'; } return null; }, onChanged: (val) {}, ), ), // Remove Button if (_paymentEntries.length > 1) IconButton( icon: HugeIcon( icon: HugeIcons.strokeRoundedDelete02, color: Colors.red, ), onPressed: () => _removePaymentRow(index), ), ], ), // Conditional Cheque Number field if (payment.type == 'Cheque') Padding( padding: const EdgeInsets.only(top: 20.0), child: TextFormField( controller: payment.chequeNumberController, decoration: kInputDecoration.copyWith( labelText: lang.S.of(context).chequeNumber, hintText: 'Ex: 12345689', ), ), ), const SizedBox(height: 15), ], ); } }