import 'package:flutter/material.dart'; import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/ui/app/buttons/elevated_button.dart'; import 'package:invoiceninja_flutter/ui/app/entity_header.dart'; import 'package:invoiceninja_flutter/ui/app/forms/date_picker.dart'; import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart'; import 'package:invoiceninja_flutter/ui/app/lists/list_divider.dart'; import 'package:invoiceninja_flutter/ui/app/search_text.dart'; import 'package:invoiceninja_flutter/ui/invoice/invoice_list_item.dart'; import 'package:invoiceninja_flutter/ui/transaction/view/transaction_view_vm.dart'; import 'package:invoiceninja_flutter/ui/app/view_scaffold.dart'; import 'package:invoiceninja_flutter/ui/vendor/vendor_list_item.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:invoiceninja_flutter/utils/icons.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; class TransactionView extends StatefulWidget { const TransactionView({ Key key, @required this.viewModel, @required this.isFilter, }) : super(key: key); final TransactionViewVM viewModel; final bool isFilter; @override _TransactionViewState createState() => new _TransactionViewState(); } class _TransactionViewState extends State { @override Widget build(BuildContext context) { final viewModel = widget.viewModel; final transaction = viewModel.transaction; final localization = AppLocalization.of(context); return ViewScaffold( isFilter: widget.isFilter, entity: transaction, body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ EntityHeader( entity: transaction, label: transaction.isDeposit ? localization.deposit : localization.withdrawal, value: formatNumber(transaction.amount, context, currencyId: transaction.currencyId), secondLabel: localization.date, secondValue: formatDate(transaction.date, context), ), ListDivider(), Expanded( child: _MatchDeposits( viewModel: viewModel, )), ], ), ); } } class _MatchDeposits extends StatefulWidget { const _MatchDeposits({ Key key, @required this.viewModel, }) : super(key: key); final TransactionViewVM viewModel; @override State<_MatchDeposits> createState() => _MatchDepositsState(); } class _MatchDepositsState extends State<_MatchDeposits> { TextEditingController _filterController; FocusNode _focusNode; List _invoices; List _selectedInvoices; bool _showFilter = false; String _minAmount = ''; String _maxAmount = ''; String _startDate = ''; String _endDate = ''; @override void initState() { super.initState(); _filterController = TextEditingController(); _focusNode = FocusNode(); _selectedInvoices = []; updateInvoiceList(); } void updateInvoiceList() { final state = widget.viewModel.state; final invoiceState = state.invoiceState; _invoices = invoiceState.map.values.where((invoice) { if (_selectedInvoices.isNotEmpty) { if (invoice.clientId != _selectedInvoices.first.clientId) { return false; } } if (invoice.isPaid || invoice.isDeleted) { return false; } final filter = _filterController.text; if (filter.isNotEmpty) { final client = state.clientState.get(invoice.clientId); if (!invoice.matchesFilter(filter) && !client.matchesNameOrEmail(filter)) { return false; } } if (_showFilter) { if (_minAmount.isNotEmpty) { if (invoice.balanceOrAmount < parseDouble(_minAmount)) { return false; } } if (_maxAmount.isNotEmpty) { if (invoice.balanceOrAmount > parseDouble(_maxAmount)) { return false; } } if (_startDate.isNotEmpty) { if (invoice.date.compareTo(_startDate) == -1) { return false; } } if (_endDate.isNotEmpty) { if (invoice.date.compareTo(_endDate) == 1) { return false; } } } return true; }).toList(); _invoices.sort((invoiceA, invoiceB) { /* if (_selectedInvoices.contains(invoiceA)) { return -1; } else if (_selectedInvoices.contains(invoiceB)) { return 1; } */ return invoiceB.date.compareTo(invoiceA.date); }); } bool get isFiltered { if (_minAmount.isNotEmpty || _maxAmount.isNotEmpty) { return true; } if (_startDate.isNotEmpty || _endDate.isNotEmpty) { return true; } return false; } @override void dispose() { _filterController.dispose(); _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final localization = AppLocalization.of(context); final viewModel = widget.viewModel; final transaction = viewModel.transaction; final state = viewModel.state; String currencyId; if (_selectedInvoices.isNotEmpty) { currencyId = state.clientState.get(_selectedInvoices.first.clientId).currencyId; } double totalSelected = 0; _selectedInvoices .forEach((invoice) => totalSelected += invoice.balanceOrAmount); return Column( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Expanded( child: Padding( padding: const EdgeInsets.only( left: 18, top: 12, right: 10, bottom: 12), child: SearchText( filterController: _filterController, focusNode: _focusNode, onChanged: (value) { setState(() { updateInvoiceList(); }); }, onCleared: () { setState(() { _filterController.text = ''; updateInvoiceList(); }); }, placeholder: localization.search, ), ), ), IconButton( onPressed: () { setState(() => _showFilter = !_showFilter); }, color: _showFilter || isFiltered ? state.accentColor : null, icon: Icon(Icons.filter_alt), tooltip: state.prefState.enableTooltips ? localization.filter : '', ), SizedBox(width: 8), ], ), ListDivider(), AnimatedContainer( duration: Duration(milliseconds: 200), height: _showFilter ? 138 : 0, child: Column( children: [ Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4), child: Column( children: [ Row( children: [ Expanded( child: DecoratedFormField( label: localization.minAmount, onChanged: (value) { setState(() { _minAmount = value; updateInvoiceList(); }); }, keyboardType: TextInputType.numberWithOptions(decimal: true), )), SizedBox( width: kTableColumnGap, ), Expanded( child: DecoratedFormField( label: localization.maxAmount, onChanged: (value) { setState(() { _maxAmount = value; updateInvoiceList(); }); }, keyboardType: TextInputType.numberWithOptions(decimal: true), )), ], ), Row(children: [ Expanded( child: DatePicker( labelText: localization.startDate, onSelected: (date, _) { setState(() { _startDate = date; updateInvoiceList(); }); }, selectedDate: _startDate, ), ), SizedBox(width: kTableColumnGap), Expanded( child: DatePicker( labelText: localization.endDate, onSelected: (date, _) { setState(() { _endDate = date; updateInvoiceList(); }); }, selectedDate: _endDate, ), ), ]), ], ), ), ), ListDivider(), ], ), ), Expanded( child: ListView.separated( separatorBuilder: (context, index) => ListDivider(), itemCount: _invoices.length, itemBuilder: (BuildContext context, int index) { final invoice = _invoices[index]; return InvoiceListItem( invoice: invoice, filter: _filterController.text, showCheck: true, isChecked: _selectedInvoices.contains(invoice), onTap: () => setState(() { if (_selectedInvoices.contains(invoice)) { _selectedInvoices.remove(invoice); } else { _selectedInvoices.add(invoice); } updateInvoiceList(); }), ); }, ), ), if (_selectedInvoices.isNotEmpty) Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), child: Text( '${_selectedInvoices.length} ${localization.selected} • ${formatNumber(totalSelected, context, currencyId: currencyId)}', textAlign: TextAlign.center, style: TextStyle(color: Colors.grey), ), ), ListDivider(), Padding( padding: const EdgeInsets.only( left: 20, bottom: 18, right: 20, ), child: AppButton( label: localization.convertToPayment, onPressed: _selectedInvoices.isEmpty ? null : () { final viewModel = widget.viewModel; viewModel.onConvertToPayment( context, transaction.id, _selectedInvoices.map((invoice) => invoice.id).toList(), ); }, iconData: getEntityActionIcon(EntityAction.convertToExpense), ), ) ], ); } } class _MatchWithdrawals extends StatefulWidget { const _MatchWithdrawals({ Key key, @required this.viewModel, }) : super(key: key); final TransactionViewVM viewModel; @override State<_MatchWithdrawals> createState() => _MatchWithdrawalsState(); } class _MatchWithdrawalsState extends State<_MatchWithdrawals> { TextEditingController _filterController; FocusNode _focusNode; List _vendors; List _categories; VendorEntity _selectedVendor; ExpenseCategoryEntity _selectedCategory; @override void initState() { super.initState(); _filterController = TextEditingController(); _focusNode = FocusNode(); updateVendorList(); } void updateVendorList() { final state = widget.viewModel.state; final vendorState = state.vendorState; _vendors = vendorState.map.values.where((vendor) { if (_selectedVendor != null) { if (vendor.id != _selectedVendor.id) { return false; } } if (vendor.isDeleted) { return false; } final filter = _filterController.text; if (filter.isNotEmpty) { if (!vendor.matchesFilter(filter)) { return false; } } return true; }).toList(); _vendors.sort((vendorA, vendorB) { /* if (_selectedInvoices.contains(invoiceA)) { return -1; } else if (_selectedInvoices.contains(invoiceB)) { return 1; } */ return vendorB.name.toLowerCase().compareTo(vendorA.name.toLowerCase()); }); } @override void dispose() { _filterController.dispose(); _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final localization = AppLocalization.of(context); final viewModel = widget.viewModel; final transaction = viewModel.transaction; final state = viewModel.state; return Column( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Expanded( child: Padding( padding: const EdgeInsets.only( left: 18, top: 12, right: 10, bottom: 12), child: SearchText( filterController: _filterController, focusNode: _focusNode, onChanged: (value) { setState(() { updateVendorList(); }); }, onCleared: () { setState(() { _filterController.text = ''; updateVendorList(); }); }, placeholder: localization.search, ), ), ), ], ), ListDivider(), Expanded( child: ListView.separated( separatorBuilder: (context, index) => ListDivider(), itemCount: _vendors.length, itemBuilder: (BuildContext context, int index) { final vendor = _vendors[index]; return VendorListItem( vendor: vendor, filter: _filterController.text, showCheck: true, isChecked: _selectedVendor.id == vendor.id, onTap: () => setState(() { _selectedVendor = vendor; updateVendorList(); }), ); }, ), ), ListDivider(), Padding( padding: const EdgeInsets.only( left: 20, bottom: 18, right: 20, ), child: AppButton( label: localization.convertToPayment, onPressed: _selectedVendor == null ? null : () { final viewModel = widget.viewModel; viewModel.onConvertToExpense( context, transaction.id, _selectedVendor.id, ); }, iconData: getEntityActionIcon(EntityAction.convertToExpense), ), ) ], ); } }