diff --git a/lib/constants.dart b/lib/constants.dart index 2bf534ab253..8094e05fc24 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -118,6 +118,7 @@ const String kSharedPrefUrl = 'url'; const String kSharedPrefToken = 'checksum'; const String kSharedPrefWidth = 'width'; const String kSharedPrefHeight = 'height'; +const String kSharedPrefMaximized = 'maximized'; const String kProductProPlanMonth = 'pro_plan'; const String kProductEnterprisePlanMonth_2 = 'enterprise_plan'; @@ -954,6 +955,8 @@ List kCustomLabels = [ 'payment_date', 'phone', 'po_number', + 'product', + 'products', 'quantity', 'quote', 'rate', diff --git a/lib/data/models/settings_model.dart b/lib/data/models/settings_model.dart index 636b5b749f2..8ad4867cdb2 100644 --- a/lib/data/models/settings_model.dart +++ b/lib/data/models/settings_model.dart @@ -861,6 +861,9 @@ abstract class SettingsEntity @BuiltValueField(wireName: 'quote_late_fee_percent1') double? get quoteLateFeePercent1; + @BuiltValueField(wireName: 'merge_e_invoice_to_pdf') + bool? get mergeEInvoiceToPdf; + bool? get taskRoundingEnabled => taskRoundToNearest == null ? null : taskRoundToNearest != 1; diff --git a/lib/data/models/settings_model.g.dart b/lib/data/models/settings_model.g.dart index 1d07164acf6..b766b1615f7 100644 --- a/lib/data/models/settings_model.g.dart +++ b/lib/data/models/settings_model.g.dart @@ -1674,6 +1674,13 @@ class _$SettingsEntitySerializer ..add(serializers.serialize(value, specifiedType: const FullType(double))); } + value = object.mergeEInvoiceToPdf; + if (value != null) { + result + ..add('merge_e_invoice_to_pdf') + ..add( + serializers.serialize(value, specifiedType: const FullType(bool))); + } return result; } @@ -2648,6 +2655,10 @@ class _$SettingsEntitySerializer result.quoteLateFeePercent1 = serializers.deserialize(value, specifiedType: const FullType(double)) as double?; break; + case 'merge_e_invoice_to_pdf': + result.mergeEInvoiceToPdf = serializers.deserialize(value, + specifiedType: const FullType(bool)) as bool?; + break; } } @@ -3203,6 +3214,8 @@ class _$SettingsEntity extends SettingsEntity { final double? quoteLateFeeAmount1; @override final double? quoteLateFeePercent1; + @override + final bool? mergeEInvoiceToPdf; factory _$SettingsEntity([void Function(SettingsEntityBuilder)? updates]) => (new SettingsEntityBuilder()..update(updates))._build(); @@ -3445,7 +3458,8 @@ class _$SettingsEntity extends SettingsEntity { this.numDaysQuoteReminder1, this.scheduleQuoteReminder1, this.quoteLateFeeAmount1, - this.quoteLateFeePercent1}) + this.quoteLateFeePercent1, + this.mergeEInvoiceToPdf}) : super._(); @override @@ -3702,7 +3716,8 @@ class _$SettingsEntity extends SettingsEntity { numDaysQuoteReminder1 == other.numDaysQuoteReminder1 && scheduleQuoteReminder1 == other.scheduleQuoteReminder1 && quoteLateFeeAmount1 == other.quoteLateFeeAmount1 && - quoteLateFeePercent1 == other.quoteLateFeePercent1; + quoteLateFeePercent1 == other.quoteLateFeePercent1 && + mergeEInvoiceToPdf == other.mergeEInvoiceToPdf; } int? __hashCode; @@ -3948,6 +3963,7 @@ class _$SettingsEntity extends SettingsEntity { _$hash = $jc(_$hash, scheduleQuoteReminder1.hashCode); _$hash = $jc(_$hash, quoteLateFeeAmount1.hashCode); _$hash = $jc(_$hash, quoteLateFeePercent1.hashCode); + _$hash = $jc(_$hash, mergeEInvoiceToPdf.hashCode); _$hash = $jf(_$hash); return __hashCode ??= _$hash; } @@ -4197,7 +4213,8 @@ class _$SettingsEntity extends SettingsEntity { ..add('numDaysQuoteReminder1', numDaysQuoteReminder1) ..add('scheduleQuoteReminder1', scheduleQuoteReminder1) ..add('quoteLateFeeAmount1', quoteLateFeeAmount1) - ..add('quoteLateFeePercent1', quoteLateFeePercent1)) + ..add('quoteLateFeePercent1', quoteLateFeePercent1) + ..add('mergeEInvoiceToPdf', mergeEInvoiceToPdf)) .toString(); } } @@ -5380,6 +5397,11 @@ class SettingsEntityBuilder set quoteLateFeePercent1(double? quoteLateFeePercent1) => _$this._quoteLateFeePercent1 = quoteLateFeePercent1; + bool? _mergeEInvoiceToPdf; + bool? get mergeEInvoiceToPdf => _$this._mergeEInvoiceToPdf; + set mergeEInvoiceToPdf(bool? mergeEInvoiceToPdf) => + _$this._mergeEInvoiceToPdf = mergeEInvoiceToPdf; + SettingsEntityBuilder(); SettingsEntityBuilder get _$this { @@ -5623,6 +5645,7 @@ class SettingsEntityBuilder _scheduleQuoteReminder1 = $v.scheduleQuoteReminder1; _quoteLateFeeAmount1 = $v.quoteLateFeeAmount1; _quoteLateFeePercent1 = $v.quoteLateFeePercent1; + _mergeEInvoiceToPdf = $v.mergeEInvoiceToPdf; _$v = null; } return this; @@ -5885,7 +5908,8 @@ class SettingsEntityBuilder numDaysQuoteReminder1: numDaysQuoteReminder1, scheduleQuoteReminder1: scheduleQuoteReminder1, quoteLateFeeAmount1: quoteLateFeeAmount1, - quoteLateFeePercent1: quoteLateFeePercent1); + quoteLateFeePercent1: quoteLateFeePercent1, + mergeEInvoiceToPdf: mergeEInvoiceToPdf); } catch (_) { late String _$failedField; try { diff --git a/lib/main.dart b/lib/main.dart index 7ea85e3ffa5..c6c9b13ac0e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -125,6 +125,10 @@ void main({bool isTesting = false}) async { ), () async { await windowManager.show(); await windowManager.focus(); + + if (prefs.getBool(kSharedPrefMaximized) == true) { + windowManager.maximize(); + } }); } diff --git a/lib/ui/app/menu_drawer.dart b/lib/ui/app/menu_drawer.dart index 7e99adea4cb..676f756d94b 100644 --- a/lib/ui/app/menu_drawer.dart +++ b/lib/ui/app/menu_drawer.dart @@ -1,5 +1,6 @@ // Dart imports: import 'dart:convert'; +import 'dart:io'; // Flutter imports: import 'package:flutter/foundation.dart'; @@ -16,6 +17,7 @@ import 'package:invoiceninja_flutter/ui/app/sms_verification.dart'; import 'package:invoiceninja_flutter/ui/app/upgrade_dialog.dart'; import 'package:invoiceninja_flutter/utils/app_review.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:redux/redux.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -1396,10 +1398,15 @@ void _showAbout(BuildContext context) async { builder: (context) => UpgradeDialog(), ); } else { + final directory = + await getApplicationDocumentsDirectory(); showMessageDialog( message: FLUTTER_VERSION['channel']!.toUpperCase() + ' • ' + - FLUTTER_VERSION['frameworkVersion']!, + FLUTTER_VERSION['frameworkVersion']! + + '\n\n${directory.path}' + + Platform.pathSeparator + + 'invoiceninja', secondaryActions: [ TextButton( child: Text(localization.logout.toUpperCase()), diff --git a/lib/ui/app/window_manager.dart b/lib/ui/app/window_manager.dart index e0532084c5d..3280edde997 100644 --- a/lib/ui/app/window_manager.dart +++ b/lib/ui/app/window_manager.dart @@ -25,10 +25,6 @@ class _WindowManagerState extends State with WindowListener { _initManager(); } - if (isApple()) { - _initWidgets(); - } - super.initState(); } @@ -37,12 +33,6 @@ class _WindowManagerState extends State with WindowListener { setState(() {}); } - void _initWidgets() async { - //print("## SET DATA"); - //await UserDefaults.setString('widgetData', 'hello', 'group.com.invoiceninja.app'); - //await WidgetKit.reloadAllTimelines(); - } - @override void onWindowResize() async { if (!isDesktopOS()) { @@ -73,6 +63,33 @@ class _WindowManagerState extends State with WindowListener { } } + @override + void onWindowMaximize() async { + if (!isDesktopOS()) { + return; + } + + final prefs = await SharedPreferences.getInstance(); + prefs.setBool(kSharedPrefMaximized, true); + } + + @override + void onWindowUnmaximize() async { + if (!isDesktopOS()) { + return; + } + + // This method is auto called when the app starts, we skip it + // if the app state hasn't been loaded yet + final store = StoreProvider.of(navigatorKey.currentContext!); + if (!store.state.isLoaded) { + return; + } + + final prefs = await SharedPreferences.getInstance(); + prefs.setBool(kSharedPrefMaximized, false); + } + @override void dispose() { if (isDesktopOS()) { diff --git a/lib/ui/expense/edit/expense_edit_settings.dart b/lib/ui/expense/edit/expense_edit_settings.dart index 975372ad880..4798fef57dd 100644 --- a/lib/ui/expense/edit/expense_edit_settings.dart +++ b/lib/ui/expense/edit/expense_edit_settings.dart @@ -184,8 +184,10 @@ class ExpenseEditSettingsState extends State { (b) => b..paymentDate = convertDateTimeToSqlDate())); } } else { - viewModel - .onChanged!(expense.rebuild((b) => b..paymentDate = '')); + viewModel.onChanged!(expense.rebuild((b) => b + ..paymentDate = '' + ..paymentTypeId = '' + ..transactionReference = '')); WidgetsBinding.instance.addPostFrameCallback((duration) { _transactionReferenceController.text = ''; }); diff --git a/lib/ui/payment_term/payment_term_screen.dart b/lib/ui/payment_term/payment_term_screen.dart index 4175a74b7f1..2cfaab2a419 100644 --- a/lib/ui/payment_term/payment_term_screen.dart +++ b/lib/ui/payment_term/payment_term_screen.dart @@ -38,8 +38,8 @@ class PaymentTermScreen extends StatelessWidget { return ListScaffold( entityType: EntityType.paymentTerm, onHamburgerLongPress: () => store.dispatch(StartPaymentTermMultiselect()), - onCancelSettingsSection: kSettingsCompanyDetails, - onCancelSettingsIndex: 3, + onCancelSettingsSection: kSettingsPaymentSettings, + onCancelSettingsIndex: 1, appBarTitle: ListFilter( key: ValueKey( '__filter_${state.paymentTermListState.filterClearedAt}__'), diff --git a/lib/ui/settings/company_details.dart b/lib/ui/settings/company_details.dart index b61b9a7c604..abebb9c79c5 100644 --- a/lib/ui/settings/company_details.dart +++ b/lib/ui/settings/company_details.dart @@ -12,7 +12,6 @@ import 'package:file_picker/file_picker.dart'; // Project imports: import 'package:invoiceninja_flutter/data/models/entities.dart'; import 'package:invoiceninja_flutter/main_app.dart'; -import 'package:invoiceninja_flutter/redux/payment_term/payment_term_selectors.dart'; import 'package:invoiceninja_flutter/redux/settings/settings_actions.dart'; import 'package:invoiceninja_flutter/redux/static/static_selectors.dart'; import 'package:invoiceninja_flutter/ui/app/blank_screen.dart'; @@ -23,7 +22,6 @@ import 'package:invoiceninja_flutter/ui/app/entity_dropdown.dart'; import 'package:invoiceninja_flutter/ui/app/form_card.dart'; import 'package:invoiceninja_flutter/ui/app/forms/app_dropdown_button.dart'; import 'package:invoiceninja_flutter/ui/app/forms/app_form.dart'; -import 'package:invoiceninja_flutter/ui/app/forms/bool_dropdown_button.dart'; import 'package:invoiceninja_flutter/ui/app/forms/custom_field.dart'; import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart'; import 'package:invoiceninja_flutter/ui/app/forms/design_picker.dart'; @@ -33,7 +31,6 @@ import 'package:invoiceninja_flutter/ui/settings/company_details_vm.dart'; import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/dialogs.dart'; import 'package:invoiceninja_flutter/utils/files.dart'; -import 'package:invoiceninja_flutter/utils/icons.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; class CompanyDetails extends StatefulWidget { @@ -592,70 +589,6 @@ class _CompanyDetailsState extends State ScrollableListView( primary: true, children: [ - FormCard( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (company.isModuleEnabled(EntityType.invoice)) - AppDropdownButton( - showBlank: true, - labelText: localization.invoicePaymentTerms, - items: memoizedDropdownPaymentTermList( - state.paymentTermState.map, - state.paymentTermState.list) - .map((paymentTermId) { - final paymentTerm = - state.paymentTermState.map[paymentTermId]!; - return DropdownMenuItem( - child: Text(paymentTerm.numDays == 0 - ? localization.dueOnReceipt - : paymentTerm.name), - value: paymentTerm.numDays.toString(), - ); - }).toList(), - value: '${settings.defaultPaymentTerms}', - onChanged: (dynamic numDays) { - viewModel.onSettingsChanged(settings.rebuild((b) => b - ..defaultPaymentTerms = - numDays == null ? null : '$numDays')); - }, - ), - if (company.isModuleEnabled(EntityType.quote)) - AppDropdownButton( - showBlank: true, - labelText: localization.quoteValidUntil, - items: memoizedDropdownPaymentTermList( - state.paymentTermState.map, - state.paymentTermState.list) - .map((paymentTermId) { - final paymentTerm = - state.paymentTermState.map[paymentTermId]!; - return DropdownMenuItem( - child: Text(paymentTerm.numDays == 0 - ? localization.dueOnReceipt - : paymentTerm.name), - value: paymentTerm.numDays.toString(), - ); - }).toList(), - value: '${settings.defaultValidUntil}', - onChanged: (dynamic numDays) { - viewModel.onSettingsChanged(settings.rebuild((b) => b - ..defaultValidUntil = - numDays == null ? null : '$numDays')); - }, - ), - ], - ), - if (!state.uiState.settingsUIState.isFiltered) - Padding( - padding: - const EdgeInsets.only(bottom: 10, left: 16, right: 16), - child: AppButton( - iconData: Icons.settings, - label: localization.configurePaymentTerms.toUpperCase(), - onPressed: () => - viewModel.onConfigurePaymentTermsPressed(context), - ), - ), if (!state.isProPlan) FormCard(children: [ if (company.isModuleEnabled(EntityType.invoice)) @@ -691,20 +624,6 @@ class _CompanyDetailsState extends State b..defaultPurchaseOrderDesignId = value!.id)), ), ]), - if (!state.settingsUIState.isFiltered) - FormCard( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - BoolDropdownButton( - value: company.useQuoteTermsOnConversion, - onChanged: (value) => viewModel.onCompanyChanged( - company.rebuild( - (b) => b..useQuoteTermsOnConversion = value)), - label: localization.useQuoteTerms, - helpLabel: localization.useQuoteTermsHelp, - iconData: getEntityIcon(EntityType.quote), - ), - ]), FormCard( isLast: true, children: [ diff --git a/lib/ui/settings/company_details_vm.dart b/lib/ui/settings/company_details_vm.dart index 9d1d4fd0243..b2efadfee5d 100644 --- a/lib/ui/settings/company_details_vm.dart +++ b/lib/ui/settings/company_details_vm.dart @@ -56,7 +56,6 @@ class CompanyDetailsVM { required this.onSavePressed, required this.onUploadLogo, required this.onDeleteLogo, - required this.onConfigurePaymentTermsPressed, required this.onUploadDocuments, }); @@ -141,13 +140,6 @@ class CompanyDetailsVM { store.dispatch(UploadLogoRequest( completer: completer, multipartFile: multipartFile, type: type)); }, - onConfigurePaymentTermsPressed: (context) { - if (state.paymentTermState.list.isEmpty) { - store.dispatch(ViewSettings(section: kSettingsPaymentTermEdit)); - } else { - store.dispatch(ViewSettings(section: kSettingsPaymentTerms)); - } - }, onUploadDocuments: (BuildContext context, List multipartFile, bool isPrivate) { final completer = Completer>(); @@ -176,6 +168,5 @@ class CompanyDetailsVM { final Function(BuildContext) onSavePressed; final Function(BuildContext, MultipartFile) onUploadLogo; final Function(BuildContext) onDeleteLogo; - final Function(BuildContext) onConfigurePaymentTermsPressed; final Function(BuildContext, List, bool) onUploadDocuments; } diff --git a/lib/ui/settings/e_invoice_settings.dart b/lib/ui/settings/e_invoice_settings.dart index f13a4e410cf..91ea04dca00 100644 --- a/lib/ui/settings/e_invoice_settings.dart +++ b/lib/ui/settings/e_invoice_settings.dart @@ -140,6 +140,14 @@ class _EInvoiceSettingsState extends State { settings.rebuild((b) => b..enableEInvoice = value)), ), if (settings.enableEInvoice == true) ...[ + BoolDropdownButton( + label: localization.mergeToPdf, + value: settings.mergeEInvoiceToPdf, + iconData: MdiIcons.callMerge, + onChanged: (value) => viewModel.onSettingsChanged( + settings.rebuild((b) => b..mergeEInvoiceToPdf = value), + ), + ), Padding( padding: EdgeInsets.only(top: settingsUIState.isFiltered ? 0 : 12), diff --git a/lib/ui/settings/payment_settings.dart b/lib/ui/settings/payment_settings.dart index b13b121f3bd..37727cc4dea 100644 --- a/lib/ui/settings/payment_settings.dart +++ b/lib/ui/settings/payment_settings.dart @@ -1,10 +1,14 @@ // Flutter imports: import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; // Project imports: import 'package:invoiceninja_flutter/data/models/company_model.dart'; import 'package:invoiceninja_flutter/data/models/entities.dart'; import 'package:invoiceninja_flutter/data/models/settings_model.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/payment_term/payment_term_selectors.dart'; +import 'package:invoiceninja_flutter/redux/settings/settings_actions.dart'; import 'package:invoiceninja_flutter/redux/static/static_selectors.dart'; import 'package:invoiceninja_flutter/ui/app/autobill_dropdown_menu_item.dart'; import 'package:invoiceninja_flutter/ui/app/buttons/elevated_button.dart'; @@ -15,6 +19,7 @@ import 'package:invoiceninja_flutter/ui/app/forms/app_dropdown_button.dart'; import 'package:invoiceninja_flutter/ui/app/forms/app_form.dart'; import 'package:invoiceninja_flutter/ui/app/forms/bool_dropdown_button.dart'; import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart'; +import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart'; import 'package:invoiceninja_flutter/ui/settings/payment_settings_vm.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; @@ -31,10 +36,12 @@ class PaymentSettings extends StatefulWidget { _PaymentSettingsState createState() => _PaymentSettingsState(); } -class _PaymentSettingsState extends State { +class _PaymentSettingsState extends State + with SingleTickerProviderStateMixin { static final GlobalKey _formKey = GlobalKey(debugLabel: '_paymentSettings'); FocusScopeNode? _focusNode; + TabController? _controller; final _minimumUnderPaymentAmountController = TextEditingController(); final _minimumPaymentAmountController = TextEditingController(); List _controllers = []; @@ -43,6 +50,21 @@ class _PaymentSettingsState extends State { void initState() { super.initState(); _focusNode = FocusScopeNode(); + + final state = widget.viewModel.state; + final settingsUIState = state.settingsUIState; + + _controller = TabController( + vsync: this, + length: 3, + initialIndex: settingsUIState.tabIndex, + ); + _controller!.addListener(_onTabChanged); + } + + void _onTabChanged() { + final store = StoreProvider.of(context); + store.dispatch(UpdateSettingsTab(tabIndex: _controller!.index)); } @override @@ -98,238 +120,334 @@ class _PaymentSettingsState extends State { return EditScaffold( title: localization.paymentSettings, onSavePressed: viewModel.onSavePressed, - body: AppForm( + appBarBottom: TabBar( + key: ValueKey(state.settingsUIState.updatedAt), + controller: _controller, + tabs: [ + Tab( + text: localization.general, + ), + Tab( + text: localization.defaults, + ), + Tab( + text: localization.emails, + ), + ], + ), + body: AppTabForm( formKey: _formKey, focusNode: _focusNode, + tabController: _controller, children: [ - FormCard( + ScrollableListView( + primary: true, children: [ - AppDropdownButton( - blankValue: null, - showBlank: true, - labelText: localization.autoBillStandardInvoices, - value: state.settingsUIState.isFiltered - ? settings.autoBillStandardInvoices - : settings.autoBillStandardInvoices ?? false, - onChanged: (dynamic value) => viewModel.onSettingsChanged( - settings - .rebuild((b) => b..autoBillStandardInvoices = value)), - items: [ - DropdownMenuItem( - child: Text(localization.enabled), value: true), - DropdownMenuItem( - child: Text(localization.off), value: false), - ]), - AppDropdownButton( - labelText: localization.autoBillRecurringInvoices, - value: settings.autoBill, - onChanged: (dynamic value) => viewModel.onSettingsChanged( - settings.rebuild((b) => b..autoBill = value)), - selectedItemBuilder: (settings.autoBill ?? '').isEmpty - ? null - : (context) => [ - SettingsEntity.AUTO_BILL_ALWAYS, - SettingsEntity.AUTO_BILL_OPT_OUT, - SettingsEntity.AUTO_BILL_OPT_IN, - SettingsEntity.AUTO_BILL_OFF, - ] - .map((type) => Text(localization.lookup(type))) - .toList(), - items: [ - SettingsEntity.AUTO_BILL_ALWAYS, - SettingsEntity.AUTO_BILL_OPT_OUT, - SettingsEntity.AUTO_BILL_OPT_IN, - SettingsEntity.AUTO_BILL_OFF - ] - .map((value) => DropdownMenuItem( - child: AutobillDropdownMenuItem(type: value), - value: value, - )) - .toList()), - AppDropdownButton( - labelText: localization.autoBillOn, - value: settings.autoBillDate, - onChanged: (dynamic value) => viewModel.onSettingsChanged( - settings.rebuild((b) => b..autoBillDate = value)), - items: [ - DropdownMenuItem( - child: Text(localization.sendDate), - value: SettingsEntity.AUTO_BILL_ON_SEND_DATE, - ), - DropdownMenuItem( - child: Text(localization.dueDate), - value: SettingsEntity.AUTO_BILL_ON_DUE_DATE, + FormCard( + children: [ + AppDropdownButton( + blankValue: null, + showBlank: true, + labelText: localization.autoBillStandardInvoices, + value: state.settingsUIState.isFiltered + ? settings.autoBillStandardInvoices + : settings.autoBillStandardInvoices ?? false, + onChanged: (dynamic value) => viewModel.onSettingsChanged( + settings.rebuild( + (b) => b..autoBillStandardInvoices = value)), + items: [ + DropdownMenuItem( + child: Text(localization.enabled), value: true), + DropdownMenuItem( + child: Text(localization.off), value: false), + ]), + AppDropdownButton( + labelText: localization.autoBillRecurringInvoices, + value: settings.autoBill, + onChanged: (dynamic value) => viewModel.onSettingsChanged( + settings.rebuild((b) => b..autoBill = value)), + selectedItemBuilder: (settings.autoBill ?? '').isEmpty + ? null + : (context) => [ + SettingsEntity.AUTO_BILL_ALWAYS, + SettingsEntity.AUTO_BILL_OPT_OUT, + SettingsEntity.AUTO_BILL_OPT_IN, + SettingsEntity.AUTO_BILL_OFF, + ] + .map( + (type) => Text(localization.lookup(type))) + .toList(), + items: [ + SettingsEntity.AUTO_BILL_ALWAYS, + SettingsEntity.AUTO_BILL_OPT_OUT, + SettingsEntity.AUTO_BILL_OPT_IN, + SettingsEntity.AUTO_BILL_OFF + ] + .map((value) => DropdownMenuItem( + child: AutobillDropdownMenuItem(type: value), + value: value, + )) + .toList()), + AppDropdownButton( + labelText: localization.autoBillOn, + value: settings.autoBillDate, + onChanged: (dynamic value) => viewModel.onSettingsChanged( + settings.rebuild((b) => b..autoBillDate = value)), + items: [ + DropdownMenuItem( + child: Text(localization.sendDate), + value: SettingsEntity.AUTO_BILL_ON_SEND_DATE, + ), + DropdownMenuItem( + child: Text(localization.dueDate), + value: SettingsEntity.AUTO_BILL_ON_DUE_DATE, + ), + ], ), + AppDropdownButton( + labelText: localization.useAvailablePayments, + value: settings.useUnappliedPayment, + onChanged: (dynamic value) { + viewModel.onSettingsChanged(settings + .rebuild((b) => b..useUnappliedPayment = value)); + }, + items: [ + DropdownMenuItem( + child: Text(localization.always), + value: CompanyEntity.USE_ALWAYS, + ), + DropdownMenuItem( + child: Text(localization.showOption), + value: CompanyEntity.USE_OPTION, + ), + DropdownMenuItem( + child: Text(localization.off), + value: CompanyEntity.USE_OFF, + ), + ]), + AppDropdownButton( + labelText: localization.useAvailableCredits, + value: settings.useCreditsPayment, + onChanged: (dynamic value) { + viewModel.onSettingsChanged(settings + .rebuild((b) => b..useCreditsPayment = value)); + }, + items: [ + DropdownMenuItem( + child: Text(localization.always), + value: CompanyEntity.USE_ALWAYS, + ), + DropdownMenuItem( + child: Text(localization.showOption), + value: CompanyEntity.USE_OPTION, + ), + DropdownMenuItem( + child: Text(localization.off), + value: CompanyEntity.USE_OFF, + ), + ]), ], ), - AppDropdownButton( - labelText: localization.useAvailablePayments, - value: settings.useUnappliedPayment, - onChanged: (dynamic value) { - viewModel.onSettingsChanged(settings - .rebuild((b) => b..useUnappliedPayment = value)); - }, - items: [ - DropdownMenuItem( - child: Text(localization.always), - value: CompanyEntity.USE_ALWAYS, - ), - DropdownMenuItem( - child: Text(localization.showOption), - value: CompanyEntity.USE_OPTION, - ), - DropdownMenuItem( - child: Text(localization.off), - value: CompanyEntity.USE_OFF, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: AppButton( + iconData: Icons.settings, + label: localization.configureGateways.toUpperCase(), + onPressed: () => + viewModel.onConfigureGatewaysPressed(context), + ), + ), + SizedBox(height: 8), + FormCard( + isLast: true, + children: [ + if (!state.uiState.settingsUIState.isFiltered) + BoolDropdownButton( + label: localization.adminInitiatedPayments, + value: company.enableApplyingPayments, + helpLabel: localization.adminInitiatedPaymentsHelp, + onChanged: (value) => viewModel.onCompanyChanged(company + .rebuild((b) => b..enableApplyingPayments = value)), ), - ]), - AppDropdownButton( - labelText: localization.useAvailableCredits, - value: settings.useCreditsPayment, - onChanged: (dynamic value) { - viewModel.onSettingsChanged( - settings.rebuild((b) => b..useCreditsPayment = value)); - }, - items: [ - DropdownMenuItem( - child: Text(localization.always), - value: CompanyEntity.USE_ALWAYS, + BoolDropdownButton( + label: localization.clientInitiatedPayments, + value: settings.clientInitiatedPayments, + helpLabel: localization.clientInitiatedPaymentsHelp, + onChanged: (value) => viewModel.onSettingsChanged(settings + .rebuild((b) => b..clientInitiatedPayments = value)), + ), + if (settings.clientInitiatedPayments == true) + Padding( + padding: const EdgeInsets.only(top: 16), + child: DecoratedFormField( + label: localization.minimumPaymentAmount, + controller: _minimumPaymentAmountController, + isMoney: true, + keyboardType: TextInputType.numberWithOptions( + decimal: true, signed: true), + ), ), - DropdownMenuItem( - child: Text(localization.showOption), - value: CompanyEntity.USE_OPTION, + BoolDropdownButton( + label: localization.allowOverPayment, + value: settings.clientPortalAllowOverPayment, + helpLabel: localization.allowOverPaymentHelp, + onChanged: (value) => viewModel.onSettingsChanged( + settings.rebuild( + (b) => b..clientPortalAllowOverPayment = value)), + ), + BoolDropdownButton( + label: localization.allowUnderPayment, + value: settings.clientPortalAllowUnderPayment, + helpLabel: localization.allowUnderPaymentHelp, + onChanged: (value) => viewModel.onSettingsChanged( + settings.rebuild( + (b) => b..clientPortalAllowUnderPayment = value)), + ), + if (settings.clientPortalAllowUnderPayment == true) + Padding( + padding: const EdgeInsets.only(top: 16), + child: DecoratedFormField( + label: localization.minimumUnderPaymentAmount, + controller: _minimumUnderPaymentAmountController, + isMoney: true, + keyboardType: TextInputType.numberWithOptions( + decimal: true, signed: true), + ), ), - DropdownMenuItem( - child: Text(localization.off), - value: CompanyEntity.USE_OFF, + if (!state.uiState.settingsUIState.isFiltered) + BoolDropdownButton( + label: localization.convertCurrency, + value: company.convertPaymentCurrency, + helpLabel: localization.convertPaymentCurrencyHelp, + onChanged: (value) => viewModel.onCompanyChanged(company + .rebuild((b) => b..convertPaymentCurrency = value)), ), - ]), - EntityDropdown( - entityType: EntityType.paymentType, - entityList: - memoizedPaymentTypeList(state.staticState.paymentTypeMap), - labelText: localization.defaultPaymentType, - entityId: settings.defaultPaymentTypeId, - onSelected: (paymentType) => viewModel.onSettingsChanged( - settings.rebuild( - (b) => b..defaultPaymentTypeId = paymentType?.id)), + ], ), ], ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: AppButton( - iconData: Icons.settings, - label: localization.configureGateways.toUpperCase(), - onPressed: () => viewModel.onConfigureGatewaysPressed(context), - ), - ), - SizedBox(height: 8), - FormCard(children: [ - if (!state.uiState.settingsUIState.isFiltered) - BoolDropdownButton( - label: localization.adminInitiatedPayments, - value: company.enableApplyingPayments, - helpLabel: localization.adminInitiatedPaymentsHelp, - onChanged: (value) => viewModel.onCompanyChanged( - company.rebuild((b) => b..enableApplyingPayments = value)), - ), - BoolDropdownButton( - label: localization.clientInitiatedPayments, - value: settings.clientInitiatedPayments, - helpLabel: localization.clientInitiatedPaymentsHelp, - onChanged: (value) => viewModel.onSettingsChanged( - settings.rebuild((b) => b..clientInitiatedPayments = value)), - ), - if (settings.clientInitiatedPayments == true) - Padding( - padding: const EdgeInsets.only(top: 16), - child: DecoratedFormField( - label: localization.minimumPaymentAmount, - controller: _minimumPaymentAmountController, - isMoney: true, - keyboardType: TextInputType.numberWithOptions( - decimal: true, signed: true), + ScrollableListView(children: [ + FormCard( + children: [ + EntityDropdown( + entityType: EntityType.paymentType, + entityList: + memoizedPaymentTypeList(state.staticState.paymentTypeMap), + labelText: localization.defaultPaymentType, + entityId: settings.defaultPaymentTypeId, + onSelected: (paymentType) => viewModel.onSettingsChanged( + settings.rebuild( + (b) => b..defaultPaymentTypeId = paymentType?.id)), ), - ), - BoolDropdownButton( - label: localization.allowOverPayment, - value: settings.clientPortalAllowOverPayment, - helpLabel: localization.allowOverPaymentHelp, - onChanged: (value) => viewModel.onSettingsChanged(settings - .rebuild((b) => b..clientPortalAllowOverPayment = value)), - ), - BoolDropdownButton( - label: localization.allowUnderPayment, - value: settings.clientPortalAllowUnderPayment, - helpLabel: localization.allowUnderPaymentHelp, - onChanged: (value) => viewModel.onSettingsChanged(settings - .rebuild((b) => b..clientPortalAllowUnderPayment = value)), + if (company.isModuleEnabled(EntityType.invoice)) + AppDropdownButton( + showBlank: true, + labelText: localization.invoicePaymentTerms, + items: memoizedDropdownPaymentTermList( + state.paymentTermState.map, + state.paymentTermState.list) + .map((paymentTermId) { + final paymentTerm = + state.paymentTermState.map[paymentTermId]!; + return DropdownMenuItem( + child: Text(paymentTerm.numDays == 0 + ? localization.dueOnReceipt + : paymentTerm.name), + value: paymentTerm.numDays.toString(), + ); + }).toList(), + value: '${settings.defaultPaymentTerms}', + onChanged: (dynamic numDays) { + viewModel.onSettingsChanged(settings.rebuild((b) => b + ..defaultPaymentTerms = + numDays == null ? null : '$numDays')); + }, + ), + if (company.isModuleEnabled(EntityType.quote)) + AppDropdownButton( + showBlank: true, + labelText: localization.quoteValidUntil, + items: memoizedDropdownPaymentTermList( + state.paymentTermState.map, + state.paymentTermState.list) + .map((paymentTermId) { + final paymentTerm = + state.paymentTermState.map[paymentTermId]!; + return DropdownMenuItem( + child: Text(paymentTerm.numDays == 0 + ? localization.dueOnReceipt + : paymentTerm.name), + value: paymentTerm.numDays.toString(), + ); + }).toList(), + value: '${settings.defaultValidUntil}', + onChanged: (dynamic numDays) { + viewModel.onSettingsChanged(settings.rebuild((b) => b + ..defaultValidUntil = + numDays == null ? null : '$numDays')); + }, + ), + ], ), - if (settings.clientPortalAllowUnderPayment == true) + if (!state.uiState.settingsUIState.isFiltered) Padding( - padding: const EdgeInsets.only(top: 16), - child: DecoratedFormField( - label: localization.minimumUnderPaymentAmount, - controller: _minimumUnderPaymentAmountController, - isMoney: true, - keyboardType: TextInputType.numberWithOptions( - decimal: true, signed: true), + padding: const EdgeInsets.only(bottom: 10, left: 16, right: 16), + child: AppButton( + iconData: Icons.settings, + label: localization.configurePaymentTerms.toUpperCase(), + onPressed: () => + viewModel.onConfigurePaymentTermsPressed(context), ), ), - if (!state.uiState.settingsUIState.isFiltered) - BoolDropdownButton( - label: localization.convertCurrency, - value: company.convertPaymentCurrency, - helpLabel: localization.convertPaymentCurrencyHelp, - onChanged: (value) => viewModel.onCompanyChanged( - company.rebuild((b) => b..convertPaymentCurrency = value)), - ), ]), - FormCard( - isLast: true, - children: [ - BoolDropdownButton( - value: settings.clientOnlinePaymentNotification, - onChanged: (value) => viewModel.onSettingsChanged( - settings.rebuild( - (b) => b..clientOnlinePaymentNotification = value)), - label: localization.onlinePaymentEmail, - helpLabel: localization.onlinePaymentEmailHelp, - iconData: Icons.email, - ), - BoolDropdownButton( - value: settings.clientManualPaymentNotification, - onChanged: (value) => viewModel.onSettingsChanged( - settings.rebuild( - (b) => b..clientManualPaymentNotification = value)), - label: localization.manualPaymentEmail, - helpLabel: localization.manualPaymentEmailHelp, - iconData: Icons.email, - ), - BoolDropdownButton( - value: settings.clientMarkPaidPaymentNotification, - onChanged: (value) => viewModel.onSettingsChanged( - settings.rebuild( - (b) => b..clientMarkPaidPaymentNotification = value)), - label: localization.markPaidPaymentEmail, - helpLabel: localization.markPaidPaymentEmailHelp, - iconData: Icons.email, - ), - if (!state.settingsUIState.isFiltered) SizedBox(height: 10), - BoolDropdownButton( - value: state.settingsUIState.isFiltered - ? settings.paymentEmailAllContacts - : settings.paymentEmailAllContacts ?? false, - onChanged: (value) => viewModel.onSettingsChanged(settings - .rebuild((b) => b..paymentEmailAllContacts = value)), - label: localization.sendEmailsTo, - iconData: Icons.email, - enabledLabel: localization.primaryContact, - disabledLabel: localization.allContacts, + ScrollableListView( + children: [ + FormCard( + isLast: true, + children: [ + BoolDropdownButton( + value: settings.clientOnlinePaymentNotification, + onChanged: (value) => viewModel.onSettingsChanged( + settings.rebuild( + (b) => b..clientOnlinePaymentNotification = value)), + label: localization.onlinePaymentEmail, + helpLabel: localization.onlinePaymentEmailHelp, + iconData: Icons.email, + ), + BoolDropdownButton( + value: settings.clientManualPaymentNotification, + onChanged: (value) => viewModel.onSettingsChanged( + settings.rebuild( + (b) => b..clientManualPaymentNotification = value)), + label: localization.manualPaymentEmail, + helpLabel: localization.manualPaymentEmailHelp, + iconData: Icons.email, + ), + BoolDropdownButton( + value: settings.clientMarkPaidPaymentNotification, + onChanged: (value) => viewModel.onSettingsChanged( + settings.rebuild((b) => + b..clientMarkPaidPaymentNotification = value)), + label: localization.markPaidPaymentEmail, + helpLabel: localization.markPaidPaymentEmailHelp, + iconData: Icons.email, + ), + if (!state.settingsUIState.isFiltered) SizedBox(height: 10), + BoolDropdownButton( + value: state.settingsUIState.isFiltered + ? settings.paymentEmailAllContacts + : settings.paymentEmailAllContacts ?? false, + onChanged: (value) => viewModel.onSettingsChanged(settings + .rebuild((b) => b..paymentEmailAllContacts = value)), + label: localization.sendEmailsTo, + iconData: Icons.email, + enabledLabel: localization.primaryContact, + disabledLabel: localization.allContacts, + ), + ], ), ], - ) + ), ], ), ); diff --git a/lib/ui/settings/payment_settings_vm.dart b/lib/ui/settings/payment_settings_vm.dart index 929601b3830..a0c40958060 100644 --- a/lib/ui/settings/payment_settings_vm.dart +++ b/lib/ui/settings/payment_settings_vm.dart @@ -49,6 +49,7 @@ class PaymentSettingsVM { required this.onSettingsChanged, required this.settings, required this.onConfigureGatewaysPressed, + required this.onConfigurePaymentTermsPressed, }); static PaymentSettingsVM fromStore(Store store) { @@ -87,6 +88,13 @@ class PaymentSettingsVM { } }); }, + onConfigurePaymentTermsPressed: (context) { + if (state.paymentTermState.list.isEmpty) { + store.dispatch(ViewSettings(section: kSettingsPaymentTermEdit)); + } else { + store.dispatch(ViewSettings(section: kSettingsPaymentTerms)); + } + }, onConfigureGatewaysPressed: (context) { store.dispatch(ViewSettings(section: kSettingsCompanyGateways)); }, @@ -99,5 +107,6 @@ class PaymentSettingsVM { final Function(BuildContext) onSavePressed; final Function(CompanyEntity) onCompanyChanged; final Function(SettingsEntity) onSettingsChanged; + final Function(BuildContext) onConfigurePaymentTermsPressed; final Function(BuildContext) onConfigureGatewaysPressed; } diff --git a/lib/ui/settings/settings_list.dart b/lib/ui/settings/settings_list.dart index 4c72027bd0b..bf1e53bbcca 100644 --- a/lib/ui/settings/settings_list.dart +++ b/lib/ui/settings/settings_list.dart @@ -370,14 +370,12 @@ class SettingsSearch extends StatelessWidget { ], [ 'defaults', - 'payment_terms', 'invoice_terms', 'invoice_footer', 'quote_terms', 'quote_footer', 'credit_terms', 'credit_footer', - 'use_quote_terms#2022-05-17', ], [ 'default_documents', @@ -421,18 +419,23 @@ class SettingsSearch extends StatelessWidget { 'company_gateways', 'auto_bill', 'auto_bill_on', - 'payment_type', - 'online_payment_email', - 'manual_payment_email', 'use_available_credits', 'admin_initiated_payments#2022-06-06', 'allow_over_payment', 'allow_under_payment', 'auto_bill_standard_invoices#2023-01-17', 'client_initiated_payments#2023-03-20', - 'send_emails_to#2023-11-30', 'use_available_payments#2024-02-19', - ] + ], + [ + 'payment_type', + 'payment_terms', + ], + [ + 'online_payment_email', + 'manual_payment_email', + 'send_emails_to#2023-11-30', + ], ], kSettingsTaxSettings: [ [ @@ -500,6 +503,7 @@ class SettingsSearch extends StatelessWidget { ], [ 'auto_convert', + 'use_quote_terms#2022-05-17', ], ], kSettingsImportExport: [ @@ -661,6 +665,7 @@ class SettingsSearch extends StatelessWidget { kSettingsEInvoiceSettings: [ [ 'e_invoice_settings#2024-05-20', + 'merge_to_pdf#2024-07-03', ], ], kSettingsTransactionRules: [ diff --git a/lib/ui/settings/workflow_settings.dart b/lib/ui/settings/workflow_settings.dart index be74ac5013d..b2ae674fd6c 100644 --- a/lib/ui/settings/workflow_settings.dart +++ b/lib/ui/settings/workflow_settings.dart @@ -178,6 +178,16 @@ class _WorkflowSettingsState extends State settings.rebuild((b) => b..autoArchiveQuote = value)), iconData: Icons.archive, ), + if (!state.settingsUIState.isFiltered) + BoolDropdownButton( + value: company.useQuoteTermsOnConversion, + onChanged: (value) => viewModel.onCompanyChanged( + company.rebuild( + (b) => b..useQuoteTermsOnConversion = value)), + label: localization.useQuoteTerms, + helpLabel: localization.useQuoteTermsHelp, + iconData: getEntityIcon(EntityType.quote), + ), ], ), ], diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index b7a9de205ca..4afe6faa0fe 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -18,6 +18,8 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'merge_to_pdf': 'Merge to PDF', + 'emails': 'Emails', 'latest_requires_php_version': 'Note: the latest version requires PHP :version', 'quote_reminder1': 'First Quote Reminder', @@ -117515,6 +117517,14 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]!['latest_requires_php_version'] ?? _localizedValues['en']!['latest_requires_php_version']!; + String get emails => + _localizedValues[localeCode]!['emails'] ?? + _localizedValues['en']!['emails']!; + + String get mergeToPdf => + _localizedValues[localeCode]!['merge_to_pdf'] ?? + _localizedValues['en']!['merge_to_pdf']!; + // STARTER: lang field - do not remove comment String lookup(String? key, {String? overrideLocaleCode}) { diff --git a/snap/gui/invoiceninja.desktop b/snap/gui/invoiceninja.desktop index 547c37d7ab0..2cb1a0f280c 100644 --- a/snap/gui/invoiceninja.desktop +++ b/snap/gui/invoiceninja.desktop @@ -1,8 +1,8 @@ [Desktop Entry] Name=Invoice Ninja -Comment=Online Invoicing +Comment=Create invoices, accept payments, track expenses & time tasks Exec=invoiceninja Icon=${SNAP}/meta/gui/invoiceninja.png Terminal=false Type=Application -Categories=Office; \ No newline at end of file +Categories=Office;Finance; \ No newline at end of file diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 107e7f933fe..e1308398c81 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -37,4 +37,6 @@ parts: invoiceninja: source: . plugin: flutter - flutter-target: lib/main.dart \ No newline at end of file + flutter-target: lib/main.dart + organize: + invoiceninja.desktop: snap/gui/invoiceninja.desktop \ No newline at end of file