first commit
This commit is contained in:
352
lib/widgets/multipal payment mathods/multi_payment_widget.dart
Normal file
352
lib/widgets/multipal payment mathods/multi_payment_widget.dart
Normal file
@@ -0,0 +1,352 @@
|
||||
// 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<FormFieldState> typeKey = GlobalKey<FormFieldState>();
|
||||
final GlobalKey<FormFieldState> amountKey = GlobalKey<FormFieldState>();
|
||||
|
||||
PaymentEntry({this.type});
|
||||
|
||||
void dispose() {
|
||||
amountController.dispose();
|
||||
chequeNumberController.dispose();
|
||||
}
|
||||
|
||||
Map<String, dynamic> 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<PaymentsTransaction>? 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<MultiPaymentWidget> {
|
||||
List<PaymentEntry> _paymentEntries = [];
|
||||
bool _isSyncing = false;
|
||||
|
||||
/// Public method to get payment entries
|
||||
/// This can be accessed via a GlobalKey
|
||||
List<PaymentEntry> 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<DropdownMenuItem<String>> 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<DropdownMenuItem<String>> paymentTypeItems,
|
||||
{bool readonly = false, bool disableDropdown = false}) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Payment Type Dropdown
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: DropdownButtonFormField<String>(
|
||||
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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user