From 8ccd9c9d6e5f22e92e2177d00a86d2d9eecf9940 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 25 Oct 2023 12:30:18 +0300 Subject: [PATCH 001/138] Add vendor display name --- lib/data/models/vendor_model.dart | 8 +++++- lib/data/models/vendor_model.g.dart | 32 ++++++++++++++++++---- lib/ui/vendor/edit/vendor_edit_footer.dart | 4 +-- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/lib/data/models/vendor_model.dart b/lib/data/models/vendor_model.dart index 71258a796..ec742663d 100644 --- a/lib/data/models/vendor_model.dart +++ b/lib/data/models/vendor_model.dart @@ -94,6 +94,7 @@ abstract class VendorEntity extends Object number: '', isChanged: false, name: '', + displayName: '', address1: '', address2: '', city: '', @@ -164,6 +165,9 @@ abstract class VendorEntity extends Object String get name; + @BuiltValueField(wireName: 'display_name') + String get displayName; + String get address1; String get address2; @@ -469,7 +473,8 @@ abstract class VendorEntity extends Object @override String get listDisplayName { - return name; + // TODO simplify once not needed any more + return displayName.isNotEmpty ? displayName : name; } @override @@ -520,6 +525,7 @@ abstract class VendorEntity extends Object ..activities.replace(BuiltList()) ..lastLogin = 0 ..languageId = '' + ..displayName = '' ..classification = ''; static Serializer get serializer => _$vendorEntitySerializer; diff --git a/lib/data/models/vendor_model.g.dart b/lib/data/models/vendor_model.g.dart index f79da094c..3f0df4550 100644 --- a/lib/data/models/vendor_model.g.dart +++ b/lib/data/models/vendor_model.g.dart @@ -116,6 +116,9 @@ class _$VendorEntitySerializer implements StructuredSerializer { final result = [ 'name', serializers.serialize(object.name, specifiedType: const FullType(String)), + 'display_name', + serializers.serialize(object.displayName, + specifiedType: const FullType(String)), 'address1', serializers.serialize(object.address1, specifiedType: const FullType(String)), @@ -260,6 +263,10 @@ class _$VendorEntitySerializer implements StructuredSerializer { result.name = serializers.deserialize(value, specifiedType: const FullType(String))! as String; break; + case 'display_name': + result.displayName = serializers.deserialize(value, + specifiedType: const FullType(String))! as String; + break; case 'address1': result.address1 = serializers.deserialize(value, specifiedType: const FullType(String))! as String; @@ -790,6 +797,8 @@ class _$VendorEntity extends VendorEntity { @override final String name; @override + final String displayName; + @override final String address1; @override final String address2; @@ -860,6 +869,7 @@ class _$VendorEntity extends VendorEntity { _$VendorEntity._( {this.loadedAt, required this.name, + required this.displayName, required this.address1, required this.address2, required this.city, @@ -894,6 +904,8 @@ class _$VendorEntity extends VendorEntity { required this.id}) : super._() { BuiltValueNullFieldError.checkNotNull(name, r'VendorEntity', 'name'); + BuiltValueNullFieldError.checkNotNull( + displayName, r'VendorEntity', 'displayName'); BuiltValueNullFieldError.checkNotNull( address1, r'VendorEntity', 'address1'); BuiltValueNullFieldError.checkNotNull( @@ -958,6 +970,7 @@ class _$VendorEntity extends VendorEntity { if (identical(other, this)) return true; return other is VendorEntity && name == other.name && + displayName == other.displayName && address1 == other.address1 && address2 == other.address2 && city == other.city && @@ -998,6 +1011,7 @@ class _$VendorEntity extends VendorEntity { if (__hashCode != null) return __hashCode!; var _$hash = 0; _$hash = $jc(_$hash, name.hashCode); + _$hash = $jc(_$hash, displayName.hashCode); _$hash = $jc(_$hash, address1.hashCode); _$hash = $jc(_$hash, address2.hashCode); _$hash = $jc(_$hash, city.hashCode); @@ -1039,6 +1053,7 @@ class _$VendorEntity extends VendorEntity { return (newBuiltValueToStringHelper(r'VendorEntity') ..add('loadedAt', loadedAt) ..add('name', name) + ..add('displayName', displayName) ..add('address1', address1) ..add('address2', address2) ..add('city', city) @@ -1087,6 +1102,10 @@ class VendorEntityBuilder String? get name => _$this._name; set name(String? name) => _$this._name = name; + String? _displayName; + String? get displayName => _$this._displayName; + set displayName(String? displayName) => _$this._displayName = displayName; + String? _address1; String? get address1 => _$this._address1; set address1(String? address1) => _$this._address1 = address1; @@ -1233,6 +1252,7 @@ class VendorEntityBuilder if ($v != null) { _loadedAt = $v.loadedAt; _name = $v.name; + _displayName = $v.displayName; _address1 = $v.address1; _address2 = $v.address2; _city = $v.city; @@ -1292,6 +1312,8 @@ class VendorEntityBuilder loadedAt: loadedAt, name: BuiltValueNullFieldError.checkNotNull( name, r'VendorEntity', 'name'), + displayName: BuiltValueNullFieldError.checkNotNull( + displayName, r'VendorEntity', 'displayName'), address1: BuiltValueNullFieldError.checkNotNull( address1, r'VendorEntity', 'address1'), address2: BuiltValueNullFieldError.checkNotNull( @@ -1304,12 +1326,10 @@ class VendorEntityBuilder postalCode, r'VendorEntity', 'postalCode'), countryId: BuiltValueNullFieldError.checkNotNull( countryId, r'VendorEntity', 'countryId'), - languageId: BuiltValueNullFieldError.checkNotNull( - languageId, r'VendorEntity', 'languageId'), - phone: BuiltValueNullFieldError.checkNotNull( - phone, r'VendorEntity', 'phone'), - privateNotes: - BuiltValueNullFieldError.checkNotNull(privateNotes, r'VendorEntity', 'privateNotes'), + languageId: + BuiltValueNullFieldError.checkNotNull(languageId, r'VendorEntity', 'languageId'), + phone: BuiltValueNullFieldError.checkNotNull(phone, r'VendorEntity', 'phone'), + privateNotes: BuiltValueNullFieldError.checkNotNull(privateNotes, r'VendorEntity', 'privateNotes'), publicNotes: BuiltValueNullFieldError.checkNotNull(publicNotes, r'VendorEntity', 'publicNotes'), website: BuiltValueNullFieldError.checkNotNull(website, r'VendorEntity', 'website'), number: BuiltValueNullFieldError.checkNotNull(number, r'VendorEntity', 'number'), diff --git a/lib/ui/vendor/edit/vendor_edit_footer.dart b/lib/ui/vendor/edit/vendor_edit_footer.dart index 4d0a46970..078e8172d 100644 --- a/lib/ui/vendor/edit/vendor_edit_footer.dart +++ b/lib/ui/vendor/edit/vendor_edit_footer.dart @@ -61,8 +61,8 @@ class VendorEditFooter extends StatelessWidget { padding: const EdgeInsets.only(left: 16, top: 8), child: Text( vendor.number.isEmpty - ? vendor.name - : '${vendor.number} • ${vendor.name}', + ? vendor.calculateDisplayName + : '${vendor.number} • ${vendor.calculateDisplayName}', style: TextStyle( color: state.prefState.enableDarkMode ? Colors.white From 229ab00c3ed265dc8a06847c90af7c51ee2a5820 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 25 Oct 2023 12:35:35 +0300 Subject: [PATCH 002/138] Fix vendor in POs --- lib/ui/invoice/edit/invoice_edit_desktop.dart | 4 ++-- lib/ui/invoice/edit/invoice_edit_details.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ui/invoice/edit/invoice_edit_desktop.dart b/lib/ui/invoice/edit/invoice_edit_desktop.dart index ae2ac2da1..9120d5684 100644 --- a/lib/ui/invoice/edit/invoice_edit_desktop.dart +++ b/lib/ui/invoice/edit/invoice_edit_desktop.dart @@ -292,8 +292,8 @@ class InvoiceEditDesktopState extends State vendorId: invoice.vendorId, vendorState: state.vendorState, onSelected: (vendor) { - viewModel.onVendorChanged!( - context, invoice, vendor as VendorEntity); + viewModel.onVendorChanged!(context, invoice, + vendor as VendorEntity?); }, onAddPressed: (completer) => viewModel .onAddVendorPressed!(context, completer), diff --git a/lib/ui/invoice/edit/invoice_edit_details.dart b/lib/ui/invoice/edit/invoice_edit_details.dart index ba6d8aac2..d382f1a93 100644 --- a/lib/ui/invoice/edit/invoice_edit_details.dart +++ b/lib/ui/invoice/edit/invoice_edit_details.dart @@ -173,7 +173,7 @@ class InvoiceEditDetailsState extends State { vendorState: state.vendorState, onSelected: (vendor) { viewModel.onVendorChanged!( - context, invoice, vendor as VendorEntity); + context, invoice, vendor as VendorEntity?); }, onAddPressed: (completer) => viewModel.onAddVendorPressed!(context, completer), From 5edaa9f23329fb5c8bcc5de2e62baf2a81d05872 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 25 Oct 2023 12:38:38 +0300 Subject: [PATCH 003/138] Fix vendor in POs --- lib/data/models/vendor_model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/data/models/vendor_model.dart b/lib/data/models/vendor_model.dart index ec742663d..be4549e44 100644 --- a/lib/data/models/vendor_model.dart +++ b/lib/data/models/vendor_model.dart @@ -474,7 +474,7 @@ abstract class VendorEntity extends Object @override String get listDisplayName { // TODO simplify once not needed any more - return displayName.isNotEmpty ? displayName : name; + return displayName.isNotEmpty ? displayName : calculateDisplayName; } @override From c84f43e4ccfb027a0057c7594e3651e00699d9a3 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 25 Oct 2023 13:13:52 +0300 Subject: [PATCH 004/138] add task based items to invoices using the mobile app #541 --- lib/data/models/invoice_model.dart | 5 +++-- lib/ui/invoice/edit/invoice_edit_items.dart | 12 +++++++++--- lib/ui/invoice/edit/invoice_item_selector.dart | 10 ++++++---- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/lib/data/models/invoice_model.dart b/lib/data/models/invoice_model.dart index 759eeca90..f96dda1b5 100644 --- a/lib/data/models/invoice_model.dart +++ b/lib/data/models/invoice_model.dart @@ -1552,7 +1552,8 @@ class TaskItemFields { abstract class InvoiceItemEntity implements Built { - factory InvoiceItemEntity({String? productKey, double? quantity}) { + factory InvoiceItemEntity( + {String? productKey, double? quantity, String? typeId}) { return _$InvoiceItemEntity._( productKey: productKey ?? '', notes: '', @@ -1565,7 +1566,7 @@ abstract class InvoiceItemEntity taxRate2: 0, taxName3: '', taxRate3: 0, - typeId: TYPE_STANDARD, + typeId: typeId ?? TYPE_STANDARD, customValue1: '', customValue2: '', customValue3: '', diff --git a/lib/ui/invoice/edit/invoice_edit_items.dart b/lib/ui/invoice/edit/invoice_edit_items.dart index e33f8163d..a60ad8ebd 100644 --- a/lib/ui/invoice/edit/invoice_edit_items.dart +++ b/lib/ui/invoice/edit/invoice_edit_items.dart @@ -252,7 +252,9 @@ class ItemEditDetailsState extends State { child: Column( children: [ DecoratedFormField( - label: localization.product, + label: widget.invoiceItem.isTask + ? localization.service + : localization.product, controller: _productKeyController, onSavePressed: widget.entityViewModel.onSavePressed, keyboardType: TextInputType.text, @@ -296,7 +298,9 @@ class ItemEditDetailsState extends State { value: _custom4Controller.text, ), DecoratedFormField( - label: localization.unitCost, + label: widget.invoiceItem.isTask + ? localization.rate + : localization.unitCost, controller: _costController, keyboardType: TextInputType.numberWithOptions(decimal: true, signed: true), @@ -304,7 +308,9 @@ class ItemEditDetailsState extends State { ), company.enableProductQuantity ? DecoratedFormField( - label: localization.quantity, + label: widget.invoiceItem.isTask + ? localization.hours + : localization.quantity, controller: _qtyController, keyboardType: TextInputType.numberWithOptions( decimal: true, signed: true), diff --git a/lib/ui/invoice/edit/invoice_item_selector.dart b/lib/ui/invoice/edit/invoice_item_selector.dart index b87956996..168fba533 100644 --- a/lib/ui/invoice/edit/invoice_item_selector.dart +++ b/lib/ui/invoice/edit/invoice_item_selector.dart @@ -42,7 +42,7 @@ class _InvoiceItemSelectorState extends State with SingleTickerProviderStateMixin { String? _filter; String? _filterClientId; - TabController? _tabController; + late TabController _tabController; final List _selected = []; final _textController = TextEditingController(); @@ -57,15 +57,17 @@ class _InvoiceItemSelectorState extends State @override void dispose() { _textController.dispose(); - _tabController!.dispose(); + _tabController.dispose(); super.dispose(); } void _addBlankItem(CompanyEntity company) { widget.onItemsSelected!([ InvoiceItemEntity( - quantity: - company.defaultQuantity || !company.enableProductQuantity ? 1 : 0) + quantity: + company.defaultQuantity || !company.enableProductQuantity ? 1 : 0, + typeId: _tabController.index == 1 ? InvoiceItemEntity.TYPE_TASK : null, + ) ]); Navigator.pop(context); } From 2e3f3444a7ded67cd305c5deb942d6620b61c7bd Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 25 Oct 2023 14:37:35 +0300 Subject: [PATCH 005/138] Fixes for FOSS version --- lib/ui/app/pinput.dart | 4 ++-- lib/ui/app/pinput.dart.foss | 6 +++--- lib/utils/oauth.dart.foss | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/ui/app/pinput.dart b/lib/ui/app/pinput.dart index 10a24f088..9126900c4 100644 --- a/lib/ui/app/pinput.dart +++ b/lib/ui/app/pinput.dart @@ -9,7 +9,7 @@ class AppPinput extends StatelessWidget { @override Widget build(BuildContext context) { - final localization = AppLocalization.of(context); + final localization = AppLocalization.of(context)!; return Pinput( onCompleted: onCompleted, @@ -18,7 +18,7 @@ class AppPinput extends StatelessWidget { showCursor: true, androidSmsAutofillMethod: AndroidSmsAutofillMethod.smsUserConsentApi, validator: (value) => - value!.isEmpty ? localization!.pleaseEnterACode : null, + value!.isEmpty ? localization.pleaseEnterACode : null, ); } } diff --git a/lib/ui/app/pinput.dart.foss b/lib/ui/app/pinput.dart.foss index aba7a3874..802a65c96 100644 --- a/lib/ui/app/pinput.dart.foss +++ b/lib/ui/app/pinput.dart.foss @@ -3,13 +3,13 @@ import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; class AppPinput extends StatelessWidget { - const AppPinput({Key key, this.onCompleted}) : super(key: key); + const AppPinput({Key? key, this.onCompleted}) : super(key: key); - final ValueChanged onCompleted; + final ValueChanged? onCompleted; @override Widget build(BuildContext context) { - final localization = AppLocalization.of(context); + final localization = AppLocalization.of(context)!; return DecoratedFormField( label: localization.code, diff --git a/lib/utils/oauth.dart.foss b/lib/utils/oauth.dart.foss index cc3c38e51..5fba624c8 100644 --- a/lib/utils/oauth.dart.foss +++ b/lib/utils/oauth.dart.foss @@ -20,11 +20,11 @@ class GoogleOAuth { } */ - static void signOut() async { + static Future signOut() async { // } - static void disconnect() async { + static Future disconnect() async { // } } From f937acd76fe6c656642641171a4ed9ade65143e0 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 25 Oct 2023 14:44:36 +0300 Subject: [PATCH 006/138] Fixes for FOSS build --- lib/utils/app_review.dart.foss | 4 ++-- lib/utils/oauth.dart.foss | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/utils/app_review.dart.foss b/lib/utils/app_review.dart.foss index 713b9af7b..06697eb2a 100644 --- a/lib/utils/app_review.dart.foss +++ b/lib/utils/app_review.dart.foss @@ -7,6 +7,6 @@ class AppReview { static void requestReview() => null; - static void openStoreListing() => - launch(getRateAppURL(navigatorKey.currentContext)); + static void openStoreListing() => launchUrl( + Uri.dataFromString(getRateAppURL(navigatorKey.currentContext!))); } diff --git a/lib/utils/oauth.dart.foss b/lib/utils/oauth.dart.foss index 5fba624c8..ac91b1f04 100644 --- a/lib/utils/oauth.dart.foss +++ b/lib/utils/oauth.dart.foss @@ -1,17 +1,17 @@ class GoogleOAuth { - static bool get isEnabled => false; - static Future signIn(Function(String, String) callback, {bool isSilent = false}) async { - // + static Future signIn(Function(String, String) callback, + {bool isSilent = false}) async { + return false; } static Future signUp(Function(String, String) callback) async { - // + return false; } static Future requestGmailScope() async { - // + return false; } /* @@ -20,11 +20,11 @@ class GoogleOAuth { } */ - static Future signOut() async { + static Future signOut() async { // } - static Future disconnect() async { + static Future disconnect() async { // } -} +} \ No newline at end of file From e4396c462cc0f2c023bb9500cc5be8532a8c508d Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 26 Oct 2023 12:30:26 +0300 Subject: [PATCH 007/138] Pending variables change --- lib/ui/app/variables.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/ui/app/variables.dart b/lib/ui/app/variables.dart index 10f5bcaca..b5be561e2 100644 --- a/lib/ui/app/variables.dart +++ b/lib/ui/app/variables.dart @@ -114,6 +114,24 @@ class _VariablesHelpState extends State InvoiceFields.footer, 'payments', ], + /* + if (company.hasCustomField(CustomFieldType.invoice1)) + 'invoice.custom1', + if (company.hasCustomField(CustomFieldType.invoice2)) + 'invoice.custom2', + if (company.hasCustomField(CustomFieldType.invoice3)) + 'invoice.custom3', + if (company.hasCustomField(CustomFieldType.invoice4)) + 'invoice.custom4', + if (company.hasCustomField(CustomFieldType.surcharge1)) + 'invoice.custom_surcharge1', + if (company.hasCustomField(CustomFieldType.surcharge2)) + 'invoice.custom_surcharge2', + if (company.hasCustomField(CustomFieldType.surcharge3)) + 'invoice.custom_surcharge3', + if (company.hasCustomField(CustomFieldType.surcharge4)) + 'invoice.custom_surcharge4', + */ if (company.hasCustomField(CustomFieldType.invoice1)) InvoiceFields.customValue1, if (company.hasCustomField(CustomFieldType.invoice2)) From c4f296606fe9e328f47ff117aa764487d36f9202 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 26 Oct 2023 12:35:01 +0300 Subject: [PATCH 008/138] Template page missing some variables (e.g. company.city, company.postal_code) #552 --- lib/data/models/company_model.dart | 2 ++ lib/ui/app/variables.dart | 2 ++ 2 files changed, 4 insertions(+) diff --git a/lib/data/models/company_model.dart b/lib/data/models/company_model.dart index 97336d710..be54f7bad 100644 --- a/lib/data/models/company_model.dart +++ b/lib/data/models/company_model.dart @@ -29,6 +29,8 @@ class CompanyFields { static const String email = 'email'; static const String address1 = 'address1'; static const String address2 = 'address2'; + static const String city = 'city'; + static const String postalCode = 'postal_code'; static const String country = 'country'; static const String vatNumber = 'vat_number'; static const String idNumber = 'id_number'; diff --git a/lib/ui/app/variables.dart b/lib/ui/app/variables.dart index b5be561e2..74d812863 100644 --- a/lib/ui/app/variables.dart +++ b/lib/ui/app/variables.dart @@ -202,6 +202,8 @@ class _VariablesHelpState extends State CompanyFields.country, CompanyFields.address1, CompanyFields.address2, + CompanyFields.city, + CompanyFields.postalCode, CompanyFields.idNumber, CompanyFields.email, CompanyFields.phone, From ab10e4835d0caac7f04ad214e929c3c8697f6aad Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 26 Oct 2023 12:35:43 +0300 Subject: [PATCH 009/138] Pending variables change --- lib/ui/app/variables.dart | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/lib/ui/app/variables.dart b/lib/ui/app/variables.dart index 74d812863..6bc24d4a2 100644 --- a/lib/ui/app/variables.dart +++ b/lib/ui/app/variables.dart @@ -114,24 +114,6 @@ class _VariablesHelpState extends State InvoiceFields.footer, 'payments', ], - /* - if (company.hasCustomField(CustomFieldType.invoice1)) - 'invoice.custom1', - if (company.hasCustomField(CustomFieldType.invoice2)) - 'invoice.custom2', - if (company.hasCustomField(CustomFieldType.invoice3)) - 'invoice.custom3', - if (company.hasCustomField(CustomFieldType.invoice4)) - 'invoice.custom4', - if (company.hasCustomField(CustomFieldType.surcharge1)) - 'invoice.custom_surcharge1', - if (company.hasCustomField(CustomFieldType.surcharge2)) - 'invoice.custom_surcharge2', - if (company.hasCustomField(CustomFieldType.surcharge3)) - 'invoice.custom_surcharge3', - if (company.hasCustomField(CustomFieldType.surcharge4)) - 'invoice.custom_surcharge4', - */ if (company.hasCustomField(CustomFieldType.invoice1)) InvoiceFields.customValue1, if (company.hasCustomField(CustomFieldType.invoice2)) From ffb3e97890968f2f32df77fd3af6b5a0d10c4ad7 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 26 Oct 2023 12:49:43 +0300 Subject: [PATCH 010/138] Add password UI for vendor contacts --- lib/data/models/vendor_model.dart | 4 +++ lib/data/models/vendor_model.g.dart | 29 +++++++++++++++++--- lib/ui/vendor/edit/vendor_edit_contacts.dart | 18 ++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/lib/data/models/vendor_model.dart b/lib/data/models/vendor_model.dart index be4549e44..2e3c2c143 100644 --- a/lib/data/models/vendor_model.dart +++ b/lib/data/models/vendor_model.dart @@ -555,6 +555,7 @@ abstract class VendorContactEntity extends Object customValue3: '', customValue4: '', link: '', + password: '', ); } @@ -585,6 +586,8 @@ abstract class VendorContactEntity extends Object String get phone; + String get password; + @BuiltValueField(wireName: 'custom_value1') String get customValue1; @@ -665,6 +668,7 @@ abstract class VendorContactEntity extends Object static void _initializeBuilder(VendorContactEntityBuilder builder) => builder ..sendEmail = true ..link = '' + ..password = '' ..customValue1 = '' ..customValue2 = '' ..customValue3 = '' diff --git a/lib/data/models/vendor_model.g.dart b/lib/data/models/vendor_model.g.dart index 3f0df4550..fe79c68f0 100644 --- a/lib/data/models/vendor_model.g.dart +++ b/lib/data/models/vendor_model.g.dart @@ -441,6 +441,9 @@ class _$VendorContactEntitySerializer 'phone', serializers.serialize(object.phone, specifiedType: const FullType(String)), + 'password', + serializers.serialize(object.password, + specifiedType: const FullType(String)), 'custom_value1', serializers.serialize(object.customValue1, specifiedType: const FullType(String)), @@ -535,6 +538,10 @@ class _$VendorContactEntitySerializer result.phone = serializers.deserialize(value, specifiedType: const FullType(String))! as String; break; + case 'password': + result.password = serializers.deserialize(value, + specifiedType: const FullType(String))! as String; + break; case 'custom_value1': result.customValue1 = serializers.deserialize(value, specifiedType: const FullType(String))! as String; @@ -1387,6 +1394,8 @@ class _$VendorContactEntity extends VendorContactEntity { @override final String phone; @override + final String password; + @override final String customValue1; @override final String customValue2; @@ -1424,6 +1433,7 @@ class _$VendorContactEntity extends VendorContactEntity { required this.isPrimary, required this.sendEmail, required this.phone, + required this.password, required this.customValue1, required this.customValue2, required this.customValue3, @@ -1450,6 +1460,8 @@ class _$VendorContactEntity extends VendorContactEntity { sendEmail, r'VendorContactEntity', 'sendEmail'); BuiltValueNullFieldError.checkNotNull( phone, r'VendorContactEntity', 'phone'); + BuiltValueNullFieldError.checkNotNull( + password, r'VendorContactEntity', 'password'); BuiltValueNullFieldError.checkNotNull( customValue1, r'VendorContactEntity', 'customValue1'); BuiltValueNullFieldError.checkNotNull( @@ -1487,6 +1499,7 @@ class _$VendorContactEntity extends VendorContactEntity { isPrimary == other.isPrimary && sendEmail == other.sendEmail && phone == other.phone && + password == other.password && customValue1 == other.customValue1 && customValue2 == other.customValue2 && customValue3 == other.customValue3 && @@ -1513,6 +1526,7 @@ class _$VendorContactEntity extends VendorContactEntity { _$hash = $jc(_$hash, isPrimary.hashCode); _$hash = $jc(_$hash, sendEmail.hashCode); _$hash = $jc(_$hash, phone.hashCode); + _$hash = $jc(_$hash, password.hashCode); _$hash = $jc(_$hash, customValue1.hashCode); _$hash = $jc(_$hash, customValue2.hashCode); _$hash = $jc(_$hash, customValue3.hashCode); @@ -1539,6 +1553,7 @@ class _$VendorContactEntity extends VendorContactEntity { ..add('isPrimary', isPrimary) ..add('sendEmail', sendEmail) ..add('phone', phone) + ..add('password', password) ..add('customValue1', customValue1) ..add('customValue2', customValue2) ..add('customValue3', customValue3) @@ -1584,6 +1599,10 @@ class VendorContactEntityBuilder String? get phone => _$this._phone; set phone(String? phone) => _$this._phone = phone; + String? _password; + String? get password => _$this._password; + set password(String? password) => _$this._password = password; + String? _customValue1; String? get customValue1 => _$this._customValue1; set customValue1(String? customValue1) => _$this._customValue1 = customValue1; @@ -1651,6 +1670,7 @@ class VendorContactEntityBuilder _isPrimary = $v.isPrimary; _sendEmail = $v.sendEmail; _phone = $v.phone; + _password = $v.password; _customValue1 = $v.customValue1; _customValue2 = $v.customValue2; _customValue3 = $v.customValue3; @@ -1698,12 +1718,13 @@ class VendorContactEntityBuilder sendEmail, r'VendorContactEntity', 'sendEmail'), phone: BuiltValueNullFieldError.checkNotNull( phone, r'VendorContactEntity', 'phone'), + password: BuiltValueNullFieldError.checkNotNull( + password, r'VendorContactEntity', 'password'), customValue1: BuiltValueNullFieldError.checkNotNull( customValue1, r'VendorContactEntity', 'customValue1'), - customValue2: BuiltValueNullFieldError.checkNotNull( - customValue2, r'VendorContactEntity', 'customValue2'), - customValue3: - BuiltValueNullFieldError.checkNotNull(customValue3, r'VendorContactEntity', 'customValue3'), + customValue2: + BuiltValueNullFieldError.checkNotNull(customValue2, r'VendorContactEntity', 'customValue2'), + customValue3: BuiltValueNullFieldError.checkNotNull(customValue3, r'VendorContactEntity', 'customValue3'), customValue4: BuiltValueNullFieldError.checkNotNull(customValue4, r'VendorContactEntity', 'customValue4'), link: BuiltValueNullFieldError.checkNotNull(link, r'VendorContactEntity', 'link'), isChanged: isChanged, diff --git a/lib/ui/vendor/edit/vendor_edit_contacts.dart b/lib/ui/vendor/edit/vendor_edit_contacts.dart index d71d6d449..4e022bbd0 100644 --- a/lib/ui/vendor/edit/vendor_edit_contacts.dart +++ b/lib/ui/vendor/edit/vendor_edit_contacts.dart @@ -186,6 +186,7 @@ class VendorContactEditDetailsState extends State { final _firstNameController = TextEditingController(); final _lastNameController = TextEditingController(); final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); final _phoneController = TextEditingController(); final _custom1Controller = TextEditingController(); final _custom2Controller = TextEditingController(); @@ -216,6 +217,7 @@ class VendorContactEditDetailsState extends State { _firstNameController, _lastNameController, _emailController, + _passwordController, _phoneController, _custom1Controller, _custom2Controller, @@ -230,6 +232,7 @@ class VendorContactEditDetailsState extends State { _firstNameController.text = contact.firstName; _lastNameController.text = contact.lastName; _emailController.text = contact.email; + _passwordController.text = contact.password; _phoneController.text = contact.phone; _custom1Controller.text = contact.customValue1; _custom2Controller.text = contact.customValue2; @@ -258,6 +261,7 @@ class VendorContactEditDetailsState extends State { ..lastName = _lastNameController.text.trim() ..email = _emailController.text.trim() ..phone = _phoneController.text.trim() + ..password = _passwordController.text.trim() ..customValue1 = _custom1Controller.text.trim() ..customValue2 = _custom2Controller.text.trim() ..customValue3 = _custom3Controller.text.trim() @@ -274,6 +278,7 @@ class VendorContactEditDetailsState extends State { final localization = AppLocalization.of(context)!; final viewModel = widget.viewModel; final state = widget.vendorViewModel.state; + final company = viewModel.company!; final isFullscreen = state.prefState.isEditorFullScreen(EntityType.vendor); final column = Column( @@ -299,6 +304,19 @@ class VendorContactEditDetailsState extends State { ? localization.emailIsInvalid : null, ), + company.settings.enablePortalPassword ?? false + ? DecoratedFormField( + autocorrect: false, + controller: _passwordController, + label: localization.password, + obscureText: true, + keyboardType: TextInputType.visiblePassword, + validator: (value) => value.isNotEmpty && value.length < 8 + ? localization.passwordIsTooShort + : null, + onSavePressed: (_) => _onDoneContactPressed(), + ) + : SizedBox(), DecoratedFormField( controller: _phoneController, onSavePressed: (_) => _onDoneContactPressed(), From af01bc45716dc35110506893fb2b5df1891a58fb Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 26 Oct 2023 13:07:01 +0300 Subject: [PATCH 011/138] Wrong contact shown for Invoice & payment summaries #561 --- lib/data/models/client_model.dart | 8 ++++++++ lib/data/models/invoice_model.dart | 4 ++-- lib/data/models/vendor_model.dart | 8 ++++++++ lib/ui/invoice/view/invoice_view_overview.dart | 18 ++++++++++-------- lib/ui/payment/view/payment_view.dart | 11 ++++++++++- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/lib/data/models/client_model.dart b/lib/data/models/client_model.dart index 99b93a36d..17a4a8945 100644 --- a/lib/data/models/client_model.dart +++ b/lib/data/models/client_model.dart @@ -891,6 +891,14 @@ abstract class ClientContactEntity extends Object } } + String get emailOrFullName { + if (email.isNotEmpty) { + return email; + } else { + return fullName; + } + } + String get fullNameWithEmail { String name = fullName; diff --git a/lib/data/models/invoice_model.dart b/lib/data/models/invoice_model.dart index f96dda1b5..569453427 100644 --- a/lib/data/models/invoice_model.dart +++ b/lib/data/models/invoice_model.dart @@ -146,9 +146,9 @@ abstract class InvoiceEntity extends Object final settings = getClientSettings(state, client); double exchangeRate = 1; - if ((client?.currencyId ?? '').isNotEmpty) { + if (state != null && (client?.currencyId ?? '').isNotEmpty) { exchangeRate = getExchangeRate( - state!.staticState.currencyMap, + state.staticState.currencyMap, fromCurrencyId: state.company.currencyId, toCurrencyId: client!.currencyId, ); diff --git a/lib/data/models/vendor_model.dart b/lib/data/models/vendor_model.dart index 2e3c2c143..6a8942c12 100644 --- a/lib/data/models/vendor_model.dart +++ b/lib/data/models/vendor_model.dart @@ -616,6 +616,14 @@ abstract class VendorContactEntity extends Object } } + String get emailOrFullName { + if (email.isNotEmpty) { + return email; + } else { + return fullName; + } + } + String get fullNameWithEmail { String name = fullName; diff --git a/lib/ui/invoice/view/invoice_view_overview.dart b/lib/ui/invoice/view/invoice_view_overview.dart index e8613a756..9e152ee49 100644 --- a/lib/ui/invoice/view/invoice_view_overview.dart +++ b/lib/ui/invoice/view/invoice_view_overview.dart @@ -248,17 +248,19 @@ class InvoiceOverview extends StatelessWidget { EntityListTile( isFilter: isFilter, entity: vendor, - subtitle: vendor.primaryContact.email, + subtitle: vendor + .getContact(invoice.invitations.first.vendorContactId) + .emailOrFullName, ), ); } else if (client != null) { - widgets.add( - EntityListTile( - isFilter: isFilter, - entity: client, - subtitle: client.primaryContact.email, - ), - ); + widgets.add(EntityListTile( + isFilter: isFilter, + entity: client, + subtitle: client + .getContact(invoice.invitations.first.clientContactId) + .emailOrFullName, + )); } if (invoice.projectId.isNotEmpty) { diff --git a/lib/ui/payment/view/payment_view.dart b/lib/ui/payment/view/payment_view.dart index e4dd6d2d2..021639e5a 100644 --- a/lib/ui/payment/view/payment_view.dart +++ b/lib/ui/payment/view/payment_view.dart @@ -54,6 +54,12 @@ class _PaymentViewState extends State { transactionReference: payment.transactionReference, ); + var invoice = InvoiceEntity(client: client); + if (payment.invoicePaymentables.isNotEmpty) { + final invoiceId = payment.invoicePaymentables.first.invoiceId; + invoice = state.invoiceState.get(invoiceId!); + } + final fields = {}; /* fields[PaymentFields.paymentStatusId] = @@ -107,7 +113,10 @@ class _PaymentViewState extends State { EntityListTile( isFilter: widget.isFilter, entity: client, - subtitle: client.primaryContact.email, + subtitle: client + .getContact( + invoice.invitations.first.clientContactId) + .emailOrFullName, ), for (final paymentable in payment.invoicePaymentables) EntityListTile( From 470609b66522dcef3e6816fbba3305f35906adc2 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 26 Oct 2023 13:16:28 +0300 Subject: [PATCH 012/138] Feature Request: Support for setting download location per-device in Windows App #563 --- lib/redux/ui/pref_state.dart | 6 +++++- lib/redux/ui/pref_state.g.dart | 22 ++++++++++++++++++++++ lib/utils/files.dart | 29 +++++++++++++++++++++-------- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/lib/redux/ui/pref_state.dart b/lib/redux/ui/pref_state.dart index 56e4c182e..6bcf58845 100644 --- a/lib/redux/ui/pref_state.dart +++ b/lib/redux/ui/pref_state.dart @@ -49,6 +49,7 @@ abstract class PrefState implements Built { persistData: false, persistUI: true, enableNativeBrowser: false, + donwloadsFolder: '', statementIncludes: BuiltList([kStatementIncludePayments]), companyPrefs: BuiltMap(), sortFields: BuiltMap(), @@ -175,6 +176,8 @@ abstract class PrefState implements Built { double get textScaleFactor; + String get donwloadsFolder; + BuiltMap get sortFields; bool get enableDarkMode => darkModeType == kBrightnessSytem @@ -284,7 +287,8 @@ abstract class PrefState implements Built { ..darkModeType = kBrightnessSytem ..colorTheme = kColorThemeLight ..darkColorTheme = kColorThemeDark - ..enableDarkModeSystem = false; + ..enableDarkModeSystem = false + ..donwloadsFolder = ''; static Serializer get serializer => _$prefStateSerializer; } diff --git a/lib/redux/ui/pref_state.g.dart b/lib/redux/ui/pref_state.g.dart index 891103286..3af76cb84 100644 --- a/lib/redux/ui/pref_state.g.dart +++ b/lib/redux/ui/pref_state.g.dart @@ -229,6 +229,9 @@ class _$PrefStateSerializer implements StructuredSerializer { 'textScaleFactor', serializers.serialize(object.textScaleFactor, specifiedType: const FullType(double)), + 'donwloadsFolder', + serializers.serialize(object.donwloadsFolder, + specifiedType: const FullType(String)), 'sortFields', serializers.serialize(object.sortFields, specifiedType: const FullType(BuiltMap, const [ @@ -411,6 +414,10 @@ class _$PrefStateSerializer implements StructuredSerializer { result.textScaleFactor = serializers.deserialize(value, specifiedType: const FullType(double))! as double; break; + case 'donwloadsFolder': + result.donwloadsFolder = serializers.deserialize(value, + specifiedType: const FullType(String))! as String; + break; case 'sortFields': result.sortFields.replace(serializers.deserialize(value, specifiedType: const FullType(BuiltMap, const [ @@ -741,6 +748,8 @@ class _$PrefState extends PrefState { @override final double textScaleFactor; @override + final String donwloadsFolder; + @override final BuiltMap sortFields; @override final BuiltMap companyPrefs; @@ -786,6 +795,7 @@ class _$PrefState extends PrefState { required this.editAfterSaving, required this.enableNativeBrowser, required this.textScaleFactor, + required this.donwloadsFolder, required this.sortFields, required this.companyPrefs}) : super._() { @@ -861,6 +871,8 @@ class _$PrefState extends PrefState { enableNativeBrowser, r'PrefState', 'enableNativeBrowser'); BuiltValueNullFieldError.checkNotNull( textScaleFactor, r'PrefState', 'textScaleFactor'); + BuiltValueNullFieldError.checkNotNull( + donwloadsFolder, r'PrefState', 'donwloadsFolder'); BuiltValueNullFieldError.checkNotNull( sortFields, r'PrefState', 'sortFields'); BuiltValueNullFieldError.checkNotNull( @@ -915,6 +927,7 @@ class _$PrefState extends PrefState { editAfterSaving == other.editAfterSaving && enableNativeBrowser == other.enableNativeBrowser && textScaleFactor == other.textScaleFactor && + donwloadsFolder == other.donwloadsFolder && sortFields == other.sortFields && companyPrefs == other.companyPrefs; } @@ -961,6 +974,7 @@ class _$PrefState extends PrefState { _$hash = $jc(_$hash, editAfterSaving.hashCode); _$hash = $jc(_$hash, enableNativeBrowser.hashCode); _$hash = $jc(_$hash, textScaleFactor.hashCode); + _$hash = $jc(_$hash, donwloadsFolder.hashCode); _$hash = $jc(_$hash, sortFields.hashCode); _$hash = $jc(_$hash, companyPrefs.hashCode); _$hash = $jf(_$hash); @@ -1007,6 +1021,7 @@ class _$PrefState extends PrefState { ..add('editAfterSaving', editAfterSaving) ..add('enableNativeBrowser', enableNativeBrowser) ..add('textScaleFactor', textScaleFactor) + ..add('donwloadsFolder', donwloadsFolder) ..add('sortFields', sortFields) ..add('companyPrefs', companyPrefs)) .toString(); @@ -1199,6 +1214,11 @@ class PrefStateBuilder implements Builder { set textScaleFactor(double? textScaleFactor) => _$this._textScaleFactor = textScaleFactor; + String? _donwloadsFolder; + String? get donwloadsFolder => _$this._donwloadsFolder; + set donwloadsFolder(String? donwloadsFolder) => + _$this._donwloadsFolder = donwloadsFolder; + MapBuilder? _sortFields; MapBuilder get sortFields => _$this._sortFields ??= new MapBuilder(); @@ -1255,6 +1275,7 @@ class PrefStateBuilder implements Builder { _editAfterSaving = $v.editAfterSaving; _enableNativeBrowser = $v.enableNativeBrowser; _textScaleFactor = $v.textScaleFactor; + _donwloadsFolder = $v.donwloadsFolder; _sortFields = $v.sortFields.toBuilder(); _companyPrefs = $v.companyPrefs.toBuilder(); _$v = null; @@ -1326,6 +1347,7 @@ class PrefStateBuilder implements Builder { editAfterSaving: BuiltValueNullFieldError.checkNotNull(editAfterSaving, r'PrefState', 'editAfterSaving'), enableNativeBrowser: BuiltValueNullFieldError.checkNotNull(enableNativeBrowser, r'PrefState', 'enableNativeBrowser'), textScaleFactor: BuiltValueNullFieldError.checkNotNull(textScaleFactor, r'PrefState', 'textScaleFactor'), + donwloadsFolder: BuiltValueNullFieldError.checkNotNull(donwloadsFolder, r'PrefState', 'donwloadsFolder'), sortFields: sortFields.build(), companyPrefs: companyPrefs.build()); } catch (_) { diff --git a/lib/utils/files.dart b/lib/utils/files.dart index 0e8927df6..87e0b250a 100644 --- a/lib/utils/files.dart +++ b/lib/utils/files.dart @@ -6,8 +6,10 @@ import 'package:flutter/foundation.dart'; // Package imports: import 'package:file_picker/file_picker.dart'; +import 'package:flutter_redux/flutter_redux.dart'; import 'package:http/http.dart'; import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/utils/dialogs.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:path_provider/path_provider.dart'; @@ -83,22 +85,33 @@ Future?> _pickFiles({ } Future getAppDownloadDirectory() async { - final directory = await (isDesktopOS() - ? getDownloadsDirectory() - : getApplicationDocumentsDirectory()); + var path = ''; - if (directory == null) { - return null; + final store = StoreProvider.of(navigatorKey.currentContext!); + final state = store.state; + + if (state.prefState.donwloadsFolder.isNotEmpty) { + path = state.prefState.donwloadsFolder; + } else { + final directory = await (isDesktopOS() + ? getDownloadsDirectory() + : getApplicationDocumentsDirectory()); + + if (directory == null) { + return null; + } + + path = directory.path; } - if (!Directory(directory.path).existsSync()) { + if (!Directory(path).existsSync()) { showErrorDialog( message: AppLocalization.of(navigatorKey.currentContext!)! .directoryDoesNotExist - .replaceFirst(':value', directory.path)); + .replaceFirst(':value', path)); return null; } - return directory.path; + return path; } From ccc37f29209782e5a41f7ac5e54c660982ecc603 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 26 Oct 2023 14:26:11 +0300 Subject: [PATCH 013/138] Fix blank email label --- lib/ui/invoice/view/invoice_view_contacts.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ui/invoice/view/invoice_view_contacts.dart b/lib/ui/invoice/view/invoice_view_contacts.dart index 63e423574..f1dd49937 100644 --- a/lib/ui/invoice/view/invoice_view_contacts.dart +++ b/lib/ui/invoice/view/invoice_view_contacts.dart @@ -107,7 +107,7 @@ class _InvitationListTile extends StatelessWidget { Padding( padding: const EdgeInsets.only(bottom: 4), child: Text( - '${localization.lookup(invitation.emailStatus)}: ' + + '${localization.sent}: ' + formatDate(invitation.sentDate, context, showTime: true), ), ), From adcc1e8e1d49b8ed920d3da9dbd86a63767307c6 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 26 Oct 2023 17:55:31 +0300 Subject: [PATCH 014/138] Add email template for statements --- lib/data/models/entities.dart | 2 +- lib/data/models/entities.g.dart | 8 ++++---- lib/data/models/settings_model.dart | 4 ++++ lib/ui/settings/templates_and_reminders.dart | 11 +++++++---- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/data/models/entities.dart b/lib/data/models/entities.dart index c047c1349..3c4447834 100644 --- a/lib/data/models/entities.dart +++ b/lib/data/models/entities.dart @@ -285,6 +285,7 @@ class EmailTemplate extends EnumClass { static const EmailTemplate payment = _$payment_email; static const EmailTemplate payment_partial = _$payment_partial_email; static const EmailTemplate credit = _$credit_email; + static const EmailTemplate purchase_order = _$purchase_order; static const EmailTemplate statement = _$statement_email; static const EmailTemplate reminder1 = _$reminder1_email; static const EmailTemplate reminder2 = _$reminder2_email; @@ -293,7 +294,6 @@ class EmailTemplate extends EnumClass { static const EmailTemplate custom1 = _$custom1_email; static const EmailTemplate custom2 = _$custom2_email; static const EmailTemplate custom3 = _$custom3_email; - static const EmailTemplate purchase_order = _$purchase_order; static BuiltSet get values => _$templateValues; diff --git a/lib/data/models/entities.g.dart b/lib/data/models/entities.g.dart index 9c648e575..5eb6982e5 100644 --- a/lib/data/models/entities.g.dart +++ b/lib/data/models/entities.g.dart @@ -240,6 +240,7 @@ const EmailTemplate _$payment_email = const EmailTemplate._('payment'); const EmailTemplate _$payment_partial_email = const EmailTemplate._('payment_partial'); const EmailTemplate _$credit_email = const EmailTemplate._('credit'); +const EmailTemplate _$purchase_order = const EmailTemplate._('purchase_order'); const EmailTemplate _$statement_email = const EmailTemplate._('statement'); const EmailTemplate _$reminder1_email = const EmailTemplate._('reminder1'); const EmailTemplate _$reminder2_email = const EmailTemplate._('reminder2'); @@ -249,7 +250,6 @@ const EmailTemplate _$reminder_endless_email = const EmailTemplate _$custom1_email = const EmailTemplate._('custom1'); const EmailTemplate _$custom2_email = const EmailTemplate._('custom2'); const EmailTemplate _$custom3_email = const EmailTemplate._('custom3'); -const EmailTemplate _$purchase_order = const EmailTemplate._('purchase_order'); EmailTemplate _$templateValueOf(String name) { switch (name) { @@ -263,6 +263,8 @@ EmailTemplate _$templateValueOf(String name) { return _$payment_partial_email; case 'credit': return _$credit_email; + case 'purchase_order': + return _$purchase_order; case 'statement': return _$statement_email; case 'reminder1': @@ -279,8 +281,6 @@ EmailTemplate _$templateValueOf(String name) { return _$custom2_email; case 'custom3': return _$custom3_email; - case 'purchase_order': - return _$purchase_order; default: throw new ArgumentError(name); } @@ -293,6 +293,7 @@ final BuiltSet _$templateValues = _$payment_email, _$payment_partial_email, _$credit_email, + _$purchase_order, _$statement_email, _$reminder1_email, _$reminder2_email, @@ -301,7 +302,6 @@ final BuiltSet _$templateValues = _$custom1_email, _$custom2_email, _$custom3_email, - _$purchase_order, ]); const UserPermission _$create = const UserPermission._('create'); diff --git a/lib/data/models/settings_model.dart b/lib/data/models/settings_model.dart index 00a5de4f5..de9a10eab 100644 --- a/lib/data/models/settings_model.dart +++ b/lib/data/models/settings_model.dart @@ -884,6 +884,8 @@ abstract class SettingsEntity return emailSubjectReminder3; case EmailTemplate.reminder_endless: return emailSubjectReminderEndless; + case EmailTemplate.statement: + return emailSubjectStatement; case EmailTemplate.custom1: return emailSubjectCustom1; case EmailTemplate.custom2: @@ -917,6 +919,8 @@ abstract class SettingsEntity return emailBodyReminder3; case EmailTemplate.reminder_endless: return emailBodyReminderEndless; + case EmailTemplate.statement: + return emailBodyStatement; case EmailTemplate.custom1: return emailBodyCustom1; case EmailTemplate.custom2: diff --git a/lib/ui/settings/templates_and_reminders.dart b/lib/ui/settings/templates_and_reminders.dart index b0a8d841e..1df75b397 100644 --- a/lib/ui/settings/templates_and_reminders.dart +++ b/lib/ui/settings/templates_and_reminders.dart @@ -145,6 +145,12 @@ class _TemplatesAndRemindersState extends State if (viewModel.state.company.markdownEmailEnabled && _bodyController.text.trim().startsWith('<')) { _bodyController.text = html2md.convert(_bodyController.text); + + // TODO remove this, it's currently needed to fix $start\_date + if (emailTemplate.name == EmailTemplate.statement.name) { + _bodyController.text = + _bodyController.text.replaceAll('\\_date', '_date'); + } } _bodyController.addListener(_onTextChanged); @@ -360,6 +366,7 @@ class _TemplatesAndRemindersState extends State items: EmailTemplate.values.where((value) { if ([ EmailTemplate.invoice, + EmailTemplate.statement, EmailTemplate.payment, EmailTemplate.payment_partial, ].contains(value) && @@ -375,10 +382,6 @@ class _TemplatesAndRemindersState extends State !company.isModuleEnabled(EntityType.purchaseOrder)) { return false; } - // TODO remove this once statements are enabled - if (value == EmailTemplate.statement) { - return false; - } return true; }).map((item) { var name = localization.lookup(item.name); From 16ccd02ad81801fdc78584e4d41360718d19838d Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sat, 28 Oct 2023 20:00:00 +0300 Subject: [PATCH 015/138] Fix for web AP --- .github/workflows/build.yml | 2 +- lib/generated_plugin_registrant.dart | 37 ---------------------------- 2 files changed, 1 insertion(+), 38 deletions(-) delete mode 100644 lib/generated_plugin_registrant.dart diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f26f3d566..9a4657c92 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,7 +60,7 @@ jobs: rm ./public/index.html git add . git commit -m 'Admin Portal - Hosted' - #git push + git push cd .. #sentry-cli --auth-token ${{secrets.sentry_auth_token}} --url ${{secrets.sentry_url}} releases --project ${{secrets.sentry_project}} --org ${{secrets.sentry_org}} files $SENTRY_RELEASE upload-sourcemaps . --ext dart --rewrite diff --git a/lib/generated_plugin_registrant.dart b/lib/generated_plugin_registrant.dart deleted file mode 100644 index 1793813ec..000000000 --- a/lib/generated_plugin_registrant.dart +++ /dev/null @@ -1,37 +0,0 @@ -// -// Generated file. Do not edit. -// - -// ignore_for_file: directives_ordering -// ignore_for_file: lines_longer_than_80_chars -// ignore_for_file: depend_on_referenced_packages - -import 'package:file_picker/_internal/file_picker_web.dart'; -import 'package:google_sign_in_web/google_sign_in_web.dart'; -import 'package:image_cropper_for_web/image_cropper_for_web.dart'; -import 'package:image_picker_for_web/image_picker_for_web.dart'; -import 'package:package_info_plus_web/package_info_plus_web.dart'; -import 'package:printing/printing_web.dart'; -import 'package:sentry_flutter/sentry_flutter_web.dart'; -import 'package:shared_preferences_web/shared_preferences_web.dart'; -import 'package:sign_in_with_apple_web/sign_in_with_apple_web.dart'; -import 'package:smart_auth/smart_auth_web.dart'; -import 'package:url_launcher_web/url_launcher_web.dart'; - -import 'package:flutter_web_plugins/flutter_web_plugins.dart'; - -// ignore: public_member_api_docs -void registerPlugins(Registrar registrar) { - FilePickerWeb.registerWith(registrar); - GoogleSignInPlugin.registerWith(registrar); - ImageCropperPlugin.registerWith(registrar); - ImagePickerPlugin.registerWith(registrar); - PackageInfoPlugin.registerWith(registrar); - PrintingPlugin.registerWith(registrar); - SentryFlutterWeb.registerWith(registrar); - SharedPreferencesPlugin.registerWith(registrar); - SignInWithApplePlugin.registerWith(registrar); - SmartAuthWeb.registerWith(registrar); - UrlLauncherPlugin.registerWith(registrar); - registrar.registerMessageHandler(); -} From ec2b87933a520898ead508cdc52c0b1e055b41e6 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 29 Oct 2023 09:41:05 +0200 Subject: [PATCH 016/138] Feature Request: Support for setting download location per-device in Windows App #563 --- lib/redux/app/app_actions.dart | 2 + lib/redux/ui/pref_reducer.dart | 7 +++ lib/ui/settings/device_settings.dart | 76 +++++++++++++++++++++++++ lib/ui/settings/device_settings_vm.dart | 5 ++ lib/ui/settings/settings_list.dart | 1 + lib/utils/files.dart | 2 +- lib/utils/i18n.dart | 29 ++++++---- pubspec.foss.yaml | 1 + pubspec.lock | 8 +++ pubspec.yaml | 1 + 10 files changed, 121 insertions(+), 11 deletions(-) diff --git a/lib/redux/app/app_actions.dart b/lib/redux/app/app_actions.dart index 028eb2229..b0ba340ab 100644 --- a/lib/redux/app/app_actions.dart +++ b/lib/redux/app/app_actions.dart @@ -188,6 +188,7 @@ class UpdateUserPreferences implements PersistPrefs { this.enableTooltips, this.flexibleSearch, this.enableNativeBrowser, + this.downloadsFolder, this.statementIncludes, }); @@ -219,6 +220,7 @@ class UpdateUserPreferences implements PersistPrefs { final bool? enableTooltips; final bool? flexibleSearch; final bool? enableNativeBrowser; + final String? downloadsFolder; final BuiltList? statementIncludes; } diff --git a/lib/redux/ui/pref_reducer.dart b/lib/redux/ui/pref_reducer.dart index 35d419d58..b2c43077c 100644 --- a/lib/redux/ui/pref_reducer.dart +++ b/lib/redux/ui/pref_reducer.dart @@ -93,6 +93,7 @@ PrefState prefReducer( longPressReducer(state.longPressSelectionIsDefault, action) ..tapSelectedToEdit = tapSelectedToEditReducer(state.tapSelectedToEdit, action) + ..donwloadsFolder = downloadsFolderReducer(state.donwloadsFolder, action) ..requireAuthentication = requireAuthenticationReducer(state.requireAuthentication, action) ..colorTheme = colorThemeReducer(state.colorTheme, action) @@ -412,6 +413,12 @@ Reducer tapSelectedToEditReducer = combineReducers([ }), ]); +Reducer downloadsFolderReducer = combineReducers([ + TypedReducer((downloadsFolder, action) { + return action.downloadsFolder ?? downloadsFolder; + }), +]); + Reducer isPreviewVisibleReducer = combineReducers([ TypedReducer((value, action) { return !value; diff --git a/lib/ui/settings/device_settings.dart b/lib/ui/settings/device_settings.dart index f25122f13..0707724cd 100644 --- a/lib/ui/settings/device_settings.dart +++ b/lib/ui/settings/device_settings.dart @@ -1,4 +1,7 @@ // Flutter imports: +import 'dart:io'; + +import 'package:filesystem_picker/filesystem_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' hide LiveText; @@ -7,6 +10,8 @@ import 'package:flutter/services.dart' hide LiveText; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; import 'package:invoiceninja_flutter/redux/company/company_selectors.dart'; +import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart'; +import 'package:invoiceninja_flutter/utils/files.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:timeago/timeago.dart' as timeago; @@ -50,6 +55,11 @@ class _DeviceSettingsState extends State TabController? _controller; FocusScopeNode? _focusNode; + String _defaultDownloadsFolder = ''; + + final _downloadsFolderController = TextEditingController(); + + List _controllers = []; @override void initState() { @@ -61,6 +71,37 @@ class _DeviceSettingsState extends State _controller!.addListener(_onTabChanged); } + @override + void didChangeDependencies() async { + super.didChangeDependencies(); + + _controllers = [ + _downloadsFolderController, + ]; + + _controllers + .forEach((dynamic controller) => controller.removeListener(_onChanged)); + + final prefState = widget.viewModel.state.prefState; + _downloadsFolderController.text = prefState.donwloadsFolder; + + _controllers + .forEach((dynamic controller) => controller.addListener(_onChanged)); + + _defaultDownloadsFolder = prefState.donwloadsFolder.isEmpty + ? await getAppDownloadDirectory() ?? '' + : prefState.donwloadsFolder; + } + + void _onChanged() async { + widget.viewModel + .onDownloadsFolderChanged(context, _downloadsFolderController.text); + + _defaultDownloadsFolder = _downloadsFolderController.text.isEmpty + ? await getAppDownloadDirectory() ?? '' + : _downloadsFolderController.text; + } + void _onTabChanged() { final store = StoreProvider.of(context); store.dispatch(UpdateSettingsTab(tabIndex: _controller!.index)); @@ -226,6 +267,41 @@ class _DeviceSettingsState extends State ), FormCard( children: [ + if (!kIsWeb) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: DecoratedFormField( + label: localization.downloadsFolder, + keyboardType: TextInputType.text, + hint: _defaultDownloadsFolder, + controller: _downloadsFolderController, + ), + ), + SizedBox(width: 20), + OutlinedButton( + onPressed: () async { + final folder = await FilesystemPicker.open( + context: context, + fsType: FilesystemType.folder, + rootDirectory: Directory(Platform.pathSeparator), + directory: Directory(_defaultDownloadsFolder), + title: localization.downloadsFolder, + pickText: localization.saveFilesToThisFolder, + ); + + if ((folder ?? '').isNotEmpty) { + _downloadsFolderController.text = folder!; + } + }, + child: Padding( + padding: const EdgeInsets.all(10), + child: Text(localization.select), + ), + ), + ], + ), Padding( padding: const EdgeInsets.only(bottom: 10), child: AppDropdownButton( diff --git a/lib/ui/settings/device_settings_vm.dart b/lib/ui/settings/device_settings_vm.dart index dfd3de580..5756d9d05 100644 --- a/lib/ui/settings/device_settings_vm.dart +++ b/lib/ui/settings/device_settings_vm.dart @@ -61,6 +61,7 @@ class DeviceSettingsVM { required this.onEnableTouchEventsChanged, required this.onEnableTooltipsChanged, required this.onEnableFlexibleSearchChanged, + required this.onDownloadsFolderChanged, }); static DeviceSettingsVM fromStore(Store store) { @@ -98,6 +99,9 @@ class DeviceSettingsVM { onTapSelectedChanged: (context, value) async { store.dispatch(UpdateUserPreferences(tapSelectedToEdit: value)); }, + onDownloadsFolderChanged: (context, value) async { + store.dispatch(UpdateUserPreferences(downloadsFolder: value)); + }, onEnableTouchEventsChanged: (context, value) async { store.dispatch(UpdateUserPreferences(enableTouchEvents: value)); store.dispatch(UpdatedSetting()); @@ -221,5 +225,6 @@ class DeviceSettingsVM { final Function(BuildContext, bool) onEnableTooltipsChanged; final Function(BuildContext, bool) onEnableFlexibleSearchChanged; final Function(BuildContext, double) onTextScaleFactorChanged; + final Function(BuildContext, String) onDownloadsFolderChanged; final Future authenticationSupported; } diff --git a/lib/ui/settings/settings_list.dart b/lib/ui/settings/settings_list.dart index accd23cf8..4c6f3ff28 100644 --- a/lib/ui/settings/settings_list.dart +++ b/lib/ui/settings/settings_list.dart @@ -505,6 +505,7 @@ class SettingsSearch extends StatelessWidget { 'show_pdf_preview', 'pdf_preview_location#2022-10-24', 'refresh_data', + 'downloads_folder#2023-10-29' ], [ 'dark_mode', diff --git a/lib/utils/files.dart b/lib/utils/files.dart index 87e0b250a..98057df24 100644 --- a/lib/utils/files.dart +++ b/lib/utils/files.dart @@ -107,7 +107,7 @@ Future getAppDownloadDirectory() async { if (!Directory(path).existsSync()) { showErrorDialog( message: AppLocalization.of(navigatorKey.currentContext!)! - .directoryDoesNotExist + .downloadsFolderDoesNotExist .replaceFirst(':value', path)); return null; diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index 1fbff80cd..172bc804c 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -18,10 +18,12 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment -'total_invoiced_quotes': 'Invoiced Quotes', + 'save_files_to_this_folder': 'Save files to this folder', + 'downloads_folder': 'Downloads Folder', + 'total_invoiced_quotes': 'Invoiced Quotes', 'total_invoice_paid_quotes': 'Invoice Paid Quotes', - 'directory_does_not_exist': - 'The download directory does not exist :value', + 'downloads_folder_does_not_exist': + 'The downloads folder does not exist :value', 'user_logged_in_notification': 'User Logged in Notification', 'user_logged_in_notification_help': 'Send an email when logging in from a new location', @@ -109902,20 +109904,27 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]!['user_logged_in_notification_help'] ?? _localizedValues['en']!['user_logged_in_notification_help']!; - String get directoryDoesNotExist => - _localizedValues[localeCode]!['directory_does_not_exist'] ?? - _localizedValues['en']!['directory_does_not_exist']!; + String get downloadsFolderDoesNotExist => + _localizedValues[localeCode]!['downloads_folder_does_not_exist'] ?? + _localizedValues['en']!['downloads_folder_does_not_exist']!; -String get totalInvoicedQuotes => + String get totalInvoicedQuotes => _localizedValues[localeCode]!['total_invoiced_quotes'] ?? _localizedValues['en']!['total_invoiced_quotes']!; -String get totalInvoicePaidQuotes => + String get totalInvoicePaidQuotes => _localizedValues[localeCode]!['total_invoice_paid_quotes'] ?? _localizedValues['en']!['total_invoice_paid_quotes']!; - - // STARTER: lang field - do not remove comment + String get downloadsFolder => + _localizedValues[localeCode]!['downloads_folder'] ?? + _localizedValues['en']!['downloads_folder']!; + +String get saveFilesToThisFolder => + _localizedValues[localeCode]!['save_files_to_this_folder'] ?? + _localizedValues['en']!['save_files_to_this_folder']!; + + // STARTER: lang field - do not remove comment String lookup(String? key) { final lookupKey = toSnakeCase(key); diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index 4ffdf9dae..7acc0400d 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -88,6 +88,7 @@ dependencies: # quick_actions: ^0.2.1 # idb_shim: ^1.11.1+1 collection: ^1.15.0-nullsafety.4 + filesystem_picker: ^4.0.0 dependency_overrides: intl: any diff --git a/pubspec.lock b/pubspec.lock index 0044dfe05..6d4e2e58c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -386,6 +386,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+1" + filesystem_picker: + dependency: "direct main" + description: + name: filesystem_picker + sha256: "37ab68968420c2073b68e002cae786d00ef1cfe18bd2b7255640338a0c47aa9a" + url: "https://pub.dev" + source: hosted + version: "4.0.0" fixnum: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 530fd9779..209d8d69e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -94,6 +94,7 @@ dependencies: # quick_actions: ^0.2.1 # idb_shim: ^1.11.1+1 collection: ^1.15.0-nullsafety.4 + filesystem_picker: ^4.0.0 dependency_overrides: intl: any From d0916ddfe335472c9128ac1def048d4570beb958 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 29 Oct 2023 09:52:43 +0200 Subject: [PATCH 017/138] Add missing last_login lang string --- lib/utils/i18n.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index 172bc804c..2df28d4eb 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -18,6 +18,7 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'last_login': 'Last Login', 'save_files_to_this_folder': 'Save files to this folder', 'downloads_folder': 'Downloads Folder', 'total_invoiced_quotes': 'Invoiced Quotes', @@ -109920,10 +109921,14 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]!['downloads_folder'] ?? _localizedValues['en']!['downloads_folder']!; -String get saveFilesToThisFolder => + String get saveFilesToThisFolder => _localizedValues[localeCode]!['save_files_to_this_folder'] ?? _localizedValues['en']!['save_files_to_this_folder']!; + String get lastLogin => + _localizedValues[localeCode]!['last_login'] ?? + _localizedValues['en']!['last_login']!; + // STARTER: lang field - do not remove comment String lookup(String? key) { From 613cb71feb180cfae60ae4d2e7045fbfc90c148d Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 29 Oct 2023 09:53:00 +0200 Subject: [PATCH 018/138] Prevent showing blank dates --- lib/utils/formatting.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/utils/formatting.dart b/lib/utils/formatting.dart index ce12992b5..8ebb4ca69 100644 --- a/lib/utils/formatting.dart +++ b/lib/utils/formatting.dart @@ -333,8 +333,9 @@ DateTime convertSqlDateToDateTime([String? date]) { DateTime convertTimestampToDate(int? timestamp) => DateTime.fromMillisecondsSinceEpoch((timestamp ?? 0) * 1000, isUtc: true); -String convertTimestampToDateString(int? timestamp) => - convertTimestampToDate(timestamp).toIso8601String(); +String convertTimestampToDateString(int? timestamp) => (timestamp ?? 0) == 0 + ? '' + : convertTimestampToDate(timestamp).toIso8601String(); String formatDuration(Duration? duration, {bool showSeconds = true}) { final time = duration.toString().split('.')[0]; From 3130bcddb42907fa5a41a57acd2eeb833e1cb144 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 29 Oct 2023 13:32:17 +0200 Subject: [PATCH 019/138] Add record state to reports --- lib/ui/reports/client_report.dart | 4 ++++ lib/utils/i18n.dart | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/lib/ui/reports/client_report.dart b/lib/ui/reports/client_report.dart index 5a73a4288..79db920e9 100644 --- a/lib/ui/reports/client_report.dart +++ b/lib/ui/reports/client_report.dart @@ -78,6 +78,7 @@ enum ClientReportFields { routing_id, tax_exempt, classification, + record_state, } var memoizedClientReport = memo6(( @@ -366,6 +367,9 @@ ReportResult clientReport( value = AppLocalization.of(navigatorKey.currentContext!)! .lookup(client.classification); break; + case ClientReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(client.entityState); } if (!ReportResult.matchField( diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index 2df28d4eb..98324019e 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -18,6 +18,7 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'record_state': 'Record State', 'last_login': 'Last Login', 'save_files_to_this_folder': 'Save files to this folder', 'downloads_folder': 'Downloads Folder', @@ -109929,6 +109930,10 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]!['last_login'] ?? _localizedValues['en']!['last_login']!; + String get recordState => + _localizedValues[localeCode]!['record_state'] ?? + _localizedValues['en']!['record_state']!; + // STARTER: lang field - do not remove comment String lookup(String? key) { From 3385c1a1566d348d02203c8ab3222a65d0dcbae5 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 29 Oct 2023 23:22:48 +0200 Subject: [PATCH 020/138] Add record state to reports --- lib/ui/reports/contact_report.dart | 7 +++++++ lib/ui/reports/credit_item_report.dart | 7 +++++++ lib/ui/reports/credit_report.dart | 7 +++++++ lib/ui/reports/expense_report.dart | 8 ++++++++ lib/ui/reports/invoice_item_report.dart | 7 +++++++ lib/ui/reports/invoice_report.dart | 7 +++++++ lib/ui/reports/payment_report.dart | 7 +++++++ lib/ui/reports/product_report.dart | 7 +++++++ lib/ui/reports/profit_loss_report.dart | 9 +++++++++ lib/ui/reports/purchase_order_item_report.dart | 7 +++++++ lib/ui/reports/purchase_order_report.dart | 7 +++++++ lib/ui/reports/quote_item_report.dart | 7 +++++++ lib/ui/reports/quote_report.dart | 7 +++++++ lib/ui/reports/recurring_expense_report.dart | 5 +++++ lib/ui/reports/recurring_invoice_report.dart | 5 +++++ lib/ui/reports/task_report.dart | 6 ++++++ lib/ui/reports/transaction_report.dart | 6 ++++++ lib/ui/reports/vendor_report.dart | 5 +++++ 18 files changed, 121 insertions(+) diff --git a/lib/ui/reports/contact_report.dart b/lib/ui/reports/contact_report.dart index 29195e743..87873d7d9 100644 --- a/lib/ui/reports/contact_report.dart +++ b/lib/ui/reports/contact_report.dart @@ -1,7 +1,9 @@ // Package imports: import 'package:built_collection/built_collection.dart'; import 'package:collection/collection.dart' show IterableNullableExtension; +import 'package:invoiceninja_flutter/main_app.dart'; import 'package:invoiceninja_flutter/redux/reports/reports_selectors.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:memoize/memoize.dart'; // Project imports: @@ -67,6 +69,7 @@ enum ContactReportFields { is_active, created_at, updated_at, + record_state, } var memoizedContactReport = memo5(( @@ -339,6 +342,10 @@ ReportResult contactReport( case ContactReportFields.created_at: value = convertTimestampToDateString(client.createdAt); break; + case ContactReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(client.entityState); + break; } if (!ReportResult.matchField( diff --git a/lib/ui/reports/credit_item_report.dart b/lib/ui/reports/credit_item_report.dart index bd3817899..49f60bc19 100644 --- a/lib/ui/reports/credit_item_report.dart +++ b/lib/ui/reports/credit_item_report.dart @@ -2,6 +2,8 @@ import 'package:built_collection/built_collection.dart'; import 'package:collection/collection.dart' show IterableNullableExtension; import 'package:invoiceninja_flutter/utils/formatting.dart'; +import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:memoize/memoize.dart'; // Project imports: @@ -41,6 +43,7 @@ enum CreditItemReportFields { taxAmount, netTotal, currency, + record_state, } var memoizedCreditItemReport = memo6(( @@ -204,6 +207,10 @@ ReportResult lineItemReport( case CreditItemReportFields.clientIdNumber: value = client.idNumber; break; + case CreditItemReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(credit.entityState); + break; } if (!ReportResult.matchField( diff --git a/lib/ui/reports/credit_report.dart b/lib/ui/reports/credit_report.dart index 743bbaceb..b11ad0d8b 100644 --- a/lib/ui/reports/credit_report.dart +++ b/lib/ui/reports/credit_report.dart @@ -2,6 +2,8 @@ import 'package:built_collection/built_collection.dart'; import 'package:collection/collection.dart' show IterableNullableExtension; import 'package:invoiceninja_flutter/redux/reports/reports_selectors.dart'; +import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:memoize/memoize.dart'; // Project imports: @@ -77,6 +79,7 @@ enum CreditReportFields { contact_email, contact_phone, contact_name, + record_state, } var memoizedCreditReport = memo6(( @@ -357,6 +360,10 @@ ReportResult creditReport( case CreditReportFields.client_id_number: value = client.idNumber; break; + case CreditReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(credit.entityState); + break; } if (!ReportResult.matchField( diff --git a/lib/ui/reports/expense_report.dart b/lib/ui/reports/expense_report.dart index 24d054602..bbc11b5f7 100644 --- a/lib/ui/reports/expense_report.dart +++ b/lib/ui/reports/expense_report.dart @@ -3,6 +3,8 @@ import 'package:built_collection/built_collection.dart'; import 'package:collection/collection.dart' show IterableNullableExtension; import 'package:invoiceninja_flutter/redux/reports/reports_selectors.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; +import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:memoize/memoize.dart'; // Project imports: @@ -54,6 +56,7 @@ enum ExpenseReportFields { updated_at, converted_amount, status, + record_state, } var memoizedExpenseReport = memo10(( @@ -276,6 +279,11 @@ ReportResult expenseReport( break; case ExpenseReportFields.status: value = kExpenseStatuses[expense.calculatedStatusId]; + break; + case ExpenseReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(expense.entityState); + break; } if (!ReportResult.matchField( diff --git a/lib/ui/reports/invoice_item_report.dart b/lib/ui/reports/invoice_item_report.dart index f115520a1..3fee07c0b 100644 --- a/lib/ui/reports/invoice_item_report.dart +++ b/lib/ui/reports/invoice_item_report.dart @@ -2,6 +2,8 @@ import 'package:built_collection/built_collection.dart'; import 'package:collection/collection.dart' show IterableNullableExtension; import 'package:invoiceninja_flutter/utils/formatting.dart'; +import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:memoize/memoize.dart'; // Project imports: @@ -41,6 +43,7 @@ enum InvoiceItemReportFields { taxAmount, netTotal, currency, + record_state, } var memoizedInvoiceItemReport = memo6(( @@ -204,6 +207,10 @@ ReportResult lineItemReport( case InvoiceItemReportFields.clientIdNumber: value = client.idNumber; break; + case InvoiceItemReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(invoice.entityState); + break; } if (!ReportResult.matchField( diff --git a/lib/ui/reports/invoice_report.dart b/lib/ui/reports/invoice_report.dart index 01310665a..becf23747 100644 --- a/lib/ui/reports/invoice_report.dart +++ b/lib/ui/reports/invoice_report.dart @@ -2,6 +2,8 @@ import 'package:built_collection/built_collection.dart'; import 'package:collection/collection.dart' show IterableNullableExtension; import 'package:invoiceninja_flutter/redux/reports/reports_selectors.dart'; +import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:memoize/memoize.dart'; // Project imports: @@ -93,6 +95,7 @@ enum InvoiceReportFields { age_group_60, age_group_90, age_group_120, + record_state, } var memoizedInvoiceReport = memo9(( @@ -468,6 +471,10 @@ ReportResult invoiceReport( case InvoiceReportFields.age_group_120: value = invoice.isPaid || invoice.age < 120 ? 0.0 : invoice.balance; break; + case InvoiceReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(invoice.entityState); + break; } if (!ReportResult.matchField( diff --git a/lib/ui/reports/payment_report.dart b/lib/ui/reports/payment_report.dart index d902f2931..5e64c5f54 100644 --- a/lib/ui/reports/payment_report.dart +++ b/lib/ui/reports/payment_report.dart @@ -3,6 +3,8 @@ import 'package:built_collection/built_collection.dart'; import 'package:collection/collection.dart' show IterableNullableExtension; import 'package:invoiceninja_flutter/redux/reports/reports_selectors.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; +import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:memoize/memoize.dart'; // Project imports: @@ -45,6 +47,7 @@ enum PaymentReportFields { converted_amount, invoices, credits, + record_state, } var memoizedPaymentReport = memo8( @@ -270,6 +273,10 @@ ReportResult paymentReport( case PaymentReportFields.credits: value = (paymentCreditMap[payment.id] ?? []).join(', '); break; + case PaymentReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(payment.entityState); + break; } if (!ReportResult.matchField( diff --git a/lib/ui/reports/product_report.dart b/lib/ui/reports/product_report.dart index df7ae6c0f..0c909e162 100644 --- a/lib/ui/reports/product_report.dart +++ b/lib/ui/reports/product_report.dart @@ -2,6 +2,8 @@ import 'package:built_collection/built_collection.dart'; import 'package:collection/collection.dart' show IterableNullableExtension; import 'package:invoiceninja_flutter/redux/product/product_selectors.dart'; +import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/redux/reports/reports_selectors.dart'; import 'package:memoize/memoize.dart'; @@ -32,6 +34,7 @@ enum ProductReportFields { notification_threshold, created_at, updated_at, + record_state, } var memoizedProductReport = memo6(( @@ -161,6 +164,10 @@ ReportResult productReport( case ProductReportFields.created_at: value = convertTimestampToDateString(product.createdAt); break; + case ProductReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(product.entityState); + break; } if (!ReportResult.matchField( diff --git a/lib/ui/reports/profit_loss_report.dart b/lib/ui/reports/profit_loss_report.dart index 75698f1c4..2a4f6bbd9 100644 --- a/lib/ui/reports/profit_loss_report.dart +++ b/lib/ui/reports/profit_loss_report.dart @@ -35,6 +35,7 @@ enum ProfitAndLossReportFields { category, currency, transaction_reference, + record_state, } var memoizedProfitAndLossReport = memo9(( @@ -174,6 +175,10 @@ ReportResult profitAndLossReport( case ProfitAndLossReportFields.transaction_reference: value = payment.transactionReference; break; + case ProfitAndLossReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(payment.entityState); + break; } if (!ReportResult.matchField( @@ -277,6 +282,10 @@ ReportResult profitAndLossReport( case ProfitAndLossReportFields.transaction_reference: value = expense.transactionReference; break; + case ProfitAndLossReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(expense.entityState); + break; } if (!ReportResult.matchField( diff --git a/lib/ui/reports/purchase_order_item_report.dart b/lib/ui/reports/purchase_order_item_report.dart index 095cf6b9f..b83a6f8e8 100644 --- a/lib/ui/reports/purchase_order_item_report.dart +++ b/lib/ui/reports/purchase_order_item_report.dart @@ -2,6 +2,8 @@ import 'package:built_collection/built_collection.dart'; import 'package:collection/collection.dart' show IterableNullableExtension; import 'package:invoiceninja_flutter/constants.dart'; +import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:memoize/memoize.dart'; @@ -38,6 +40,7 @@ enum PurchaseOrderItemReportFields { taxAmount, netTotal, currency, + record_state, } var memoizedPurchaseOrderItemReport = memo7(( @@ -203,6 +206,10 @@ ReportResult lineItemReport( case PurchaseOrderItemReportFields.clientIdNumber: value = client.idNumber; break; + case PurchaseOrderItemReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(invoice.entityState); + break; } if (!ReportResult.matchField( diff --git a/lib/ui/reports/purchase_order_report.dart b/lib/ui/reports/purchase_order_report.dart index 32250620c..bb5217789 100644 --- a/lib/ui/reports/purchase_order_report.dart +++ b/lib/ui/reports/purchase_order_report.dart @@ -2,6 +2,8 @@ import 'package:built_collection/built_collection.dart'; import 'package:collection/collection.dart' show IterableNullableExtension; import 'package:invoiceninja_flutter/redux/reports/reports_selectors.dart'; +import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:memoize/memoize.dart'; @@ -76,6 +78,7 @@ enum PurchaseOrderReportFields { contact_email, contact_phone, contact_name, + record_state, } var memoizedPurchaseOrderReport = memo7(( @@ -357,6 +360,10 @@ ReportResult purchaseOrderReport( case PurchaseOrderReportFields.vendor_number: value = vendor.number; break; + case PurchaseOrderReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(purchaseOrder.entityState); + break; } if (!ReportResult.matchField( diff --git a/lib/ui/reports/quote_item_report.dart b/lib/ui/reports/quote_item_report.dart index 66061fa4c..5ae8f7546 100644 --- a/lib/ui/reports/quote_item_report.dart +++ b/lib/ui/reports/quote_item_report.dart @@ -1,6 +1,8 @@ // Package imports: import 'package:built_collection/built_collection.dart'; import 'package:collection/collection.dart' show IterableNullableExtension; +import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:memoize/memoize.dart'; @@ -41,6 +43,7 @@ enum QuoteItemReportFields { taxAmount, netTotal, currency, + record_state, } var memoizedQuoteItemReport = memo6(( @@ -200,6 +203,10 @@ ReportResult lineItemReport( case QuoteItemReportFields.clientIdNumber: value = client.idNumber; break; + case QuoteItemReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(invoice.entityState); + break; } if (!ReportResult.matchField( diff --git a/lib/ui/reports/quote_report.dart b/lib/ui/reports/quote_report.dart index 96f5780ce..9a2efeeb9 100644 --- a/lib/ui/reports/quote_report.dart +++ b/lib/ui/reports/quote_report.dart @@ -2,6 +2,8 @@ import 'package:built_collection/built_collection.dart'; import 'package:collection/collection.dart' show IterableNullableExtension; import 'package:invoiceninja_flutter/redux/reports/reports_selectors.dart'; +import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:memoize/memoize.dart'; @@ -75,6 +77,7 @@ enum QuoteReportFields { contact_email, contact_phone, contact_name, + record_state, } var memoizedQuoteReport = memo7(( @@ -351,6 +354,10 @@ ReportResult quoteReport( case QuoteReportFields.client_id_number: value = client.idNumber; break; + case QuoteReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(quote.entityState); + break; } if (!ReportResult.matchField( diff --git a/lib/ui/reports/recurring_expense_report.dart b/lib/ui/reports/recurring_expense_report.dart index a972f39bf..acb37b2eb 100644 --- a/lib/ui/reports/recurring_expense_report.dart +++ b/lib/ui/reports/recurring_expense_report.dart @@ -45,6 +45,7 @@ enum RecurringExpenseReportFields { frequency, start_date, remaining_cycles, + record_state, } var memoizedRecurringExpenseReport = memo9(( @@ -227,6 +228,10 @@ ReportResult recurringExpenseReport( ? localization!.endless : '${invoice.remainingCycles}'; break; + case RecurringExpenseReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(expense.entityState); + break; } if (!ReportResult.matchField( diff --git a/lib/ui/reports/recurring_invoice_report.dart b/lib/ui/reports/recurring_invoice_report.dart index dc8c59897..2489dc970 100644 --- a/lib/ui/reports/recurring_invoice_report.dart +++ b/lib/ui/reports/recurring_invoice_report.dart @@ -86,6 +86,7 @@ enum RecurringInvoiceReportFields { due_on, next_send_date, last_sent_date, + record_state, } var memoizedRecurringInvoiceReport = memo8(( @@ -401,6 +402,10 @@ ReportResult recurringInvoiceReport( .replaceFirst(':count', '${invoice.dueDateDays}'); } break; + case RecurringInvoiceReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(invoice.entityState); + break; } if (!ReportResult.matchField( diff --git a/lib/ui/reports/task_report.dart b/lib/ui/reports/task_report.dart index 3e3344af1..0a0541a93 100644 --- a/lib/ui/reports/task_report.dart +++ b/lib/ui/reports/task_report.dart @@ -1,6 +1,8 @@ // Package imports: import 'package:built_collection/built_collection.dart'; import 'package:collection/collection.dart' show IterableNullableExtension; +import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/redux/reports/reports_selectors.dart'; import 'package:memoize/memoize.dart'; @@ -43,6 +45,7 @@ enum TaskReportFields { assigned_to, created_by, amount, + record_state, } var memoizedTaskReport = memo10(( @@ -247,6 +250,9 @@ ReportResult taskReport( )!, ); break; + case TaskReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(task.entityState); } if (!ReportResult.matchField( diff --git a/lib/ui/reports/transaction_report.dart b/lib/ui/reports/transaction_report.dart index 62be3c92d..214602ae1 100644 --- a/lib/ui/reports/transaction_report.dart +++ b/lib/ui/reports/transaction_report.dart @@ -2,6 +2,8 @@ import 'package:built_collection/built_collection.dart'; import 'package:collection/collection.dart' show IterableNullableExtension; import 'package:invoiceninja_flutter/utils/strings.dart'; +import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:memoize/memoize.dart'; // Project imports: @@ -31,6 +33,7 @@ enum TransactionReportFields { defaultCategory, created_at, updated_at, + record_state, } var memoizedTransactionReport = memo10(( @@ -172,6 +175,9 @@ ReportResult transactionReport( case TransactionReportFields.created_at: value = convertTimestampToDateString(transaction.createdAt); break; + case TransactionReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(transaction.entityState); } if (!ReportResult.matchField( diff --git a/lib/ui/reports/vendor_report.dart b/lib/ui/reports/vendor_report.dart index d67cb89bf..2e85e56d2 100644 --- a/lib/ui/reports/vendor_report.dart +++ b/lib/ui/reports/vendor_report.dart @@ -56,6 +56,7 @@ enum VendorReportFields { documents, last_login, classification, + record_state, /* contact_last_login, shipping_address1, @@ -323,6 +324,10 @@ ReportResult vendorReport( value = AppLocalization.of(navigatorKey.currentContext!)! .lookup(vendor.classification); break; + case VendorReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(vendor.entityState); + break; } if (!ReportResult.matchField( From 3a421a824eb65005febb9d2d57f19d1cf594230b Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 29 Oct 2023 23:31:56 +0200 Subject: [PATCH 021/138] Fix for company logo --- lib/data/repositories/settings_repository.dart | 4 ++-- lib/data/web_client.dart | 6 +++--- lib/redux/company/company_actions.dart | 12 ++++++------ lib/redux/settings/settings_actions.dart | 4 ++-- lib/redux/settings/settings_middleware.dart | 6 +++--- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/data/repositories/settings_repository.dart b/lib/data/repositories/settings_repository.dart index b4a8706a0..2a30cc866 100644 --- a/lib/data/repositories/settings_repository.dart +++ b/lib/data/repositories/settings_repository.dart @@ -38,7 +38,7 @@ class SettingsRepository { } Future saveEInvoiceCertificate(Credentials credentials, - CompanyEntity company, MultipartFile? eInvoiceCertificate) async { + CompanyEntity company, MultipartFile eInvoiceCertificate) async { dynamic response; final url = credentials.url! + '/companies/${company.id}'; @@ -205,7 +205,7 @@ class SettingsRepository { } Future uploadLogo(Credentials credentials, String entityId, - MultipartFile? multipartFile, EntityType? type) async { + MultipartFile multipartFile, EntityType? type) async { final route = type == EntityType.company ? 'companies' : type == EntityType.group diff --git a/lib/data/web_client.dart b/lib/data/web_client.dart index d7393685d..96a18fa78 100644 --- a/lib/data/web_client.dart +++ b/lib/data/web_client.dart @@ -68,7 +68,7 @@ class WebClient { String url, String? token, { dynamic data, - List? multipartFiles, + List? multipartFiles, String? secret, String? password, String? idToken, @@ -309,12 +309,12 @@ String _parseError(int code, String response) { } Future _uploadFiles( - String url, String? token, List multipartFiles, + String url, String? token, List multipartFiles, {String method = 'POST', dynamic data}) async { final request = http.MultipartRequest(method, Uri.parse(url)) ..fields.addAll(data ?? {}) ..headers.addAll(_getHeaders(url, token)) - ..files.addAll(multipartFiles as Iterable); + ..files.addAll(multipartFiles); return await http.Response.fromStream(await request.send()) .timeout(const Duration(minutes: 10)); diff --git a/lib/redux/company/company_actions.dart b/lib/redux/company/company_actions.dart index 6aca27730..4e5ed8787 100644 --- a/lib/redux/company/company_actions.dart +++ b/lib/redux/company/company_actions.dart @@ -58,14 +58,14 @@ class SaveCompanyFailure implements StopSaving { class SaveEInvoiceCertificateRequest implements StartSaving { SaveEInvoiceCertificateRequest({ - this.completer, - this.company, - this.eInvoiceCertificate, + required this.completer, + required this.company, + required this.eInvoiceCertificate, }); - final Completer? completer; - final CompanyEntity? company; - final MultipartFile? eInvoiceCertificate; + final Completer completer; + final CompanyEntity company; + final MultipartFile eInvoiceCertificate; } class SaveEInvoiceCertificateSuccess diff --git a/lib/redux/settings/settings_actions.dart b/lib/redux/settings/settings_actions.dart index e7c532773..f158bc762 100644 --- a/lib/redux/settings/settings_actions.dart +++ b/lib/redux/settings/settings_actions.dart @@ -71,10 +71,10 @@ class UpdateUserSettings implements PersistUI { } class UploadLogoRequest implements StartSaving { - UploadLogoRequest({this.completer, this.multipartFile, this.type}); + UploadLogoRequest({this.completer, required this.multipartFile, this.type}); final Completer? completer; - final MultipartFile? multipartFile; + final MultipartFile multipartFile; final EntityType? type; } diff --git a/lib/redux/settings/settings_middleware.dart b/lib/redux/settings/settings_middleware.dart index d34a46e7c..1766a7535 100644 --- a/lib/redux/settings/settings_middleware.dart +++ b/lib/redux/settings/settings_middleware.dart @@ -126,16 +126,16 @@ Middleware _saveEInvoiceCertificate( settingsRepository .saveEInvoiceCertificate( store.state.credentials, - action.company!, + action.company, action.eInvoiceCertificate, ) .then((company) { store.dispatch(SaveEInvoiceCertificateSuccess(company)); - action.completer!.complete(); + action.completer.complete(); }).catchError((Object error) { print(error); store.dispatch(SaveEInvoiceCertificateFailure(error)); - action.completer!.completeError(error); + action.completer.completeError(error); }); next(action); From 3a899a04a6ab052dff8504427f03444dba7b2e0e Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 30 Oct 2023 13:09:56 +0200 Subject: [PATCH 022/138] Add quotes to projects --- lib/data/models/entities.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/data/models/entities.dart b/lib/data/models/entities.dart index 3c4447834..21569840f 100644 --- a/lib/data/models/entities.dart +++ b/lib/data/models/entities.dart @@ -131,6 +131,7 @@ class EntityType extends EnumClass { EntityType.task, EntityType.expense, EntityType.invoice, + EntityType.quote, ]; case EntityType.group: return [ From 53ffdcba0797c590483fbad9721a9b956f4839f8 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 30 Oct 2023 14:01:51 +0200 Subject: [PATCH 023/138] [Feature Request] Additional report for 'Task Item' #565 --- lib/constants.dart | 1 + lib/data/models/task_model.dart | 29 +- lib/redux/dashboard/dashboard_selectors.dart | 2 +- lib/redux/project/project_selectors.dart | 4 +- lib/redux/task/task_actions.dart | 4 +- lib/redux/task/task_selectors.dart | 4 +- lib/ui/reports/reports_screen.dart | 5 +- lib/ui/reports/reports_screen_vm.dart | 15 + lib/ui/reports/task_item_report.dart | 310 +++++++++++++++++++ lib/ui/task/edit/task_edit_desktop.dart | 43 ++- lib/ui/task/edit/task_edit_times.dart | 2 +- lib/ui/task/edit/task_edit_vm.dart | 2 +- lib/ui/task/task_presenter.dart | 4 +- lib/ui/task/view/task_view_vm.dart | 3 +- lib/utils/i18n.dart | 6 + 15 files changed, 383 insertions(+), 51 deletions(-) create mode 100644 lib/ui/reports/task_item_report.dart diff --git a/lib/constants.dart b/lib/constants.dart index 318dcc1ad..a083a0d9c 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -638,6 +638,7 @@ const String kReportPayment = 'payment'; const String kReportProduct = 'product'; const String kReportProfitAndLoss = 'profit_and_loss'; const String kReportTask = 'task'; +const String kReportTaskItem = 'task_item'; const String kReportInvoiceTax = 'invoice_tax'; const String kReportPaymentTax = 'payment_tax'; const String kReportQuote = 'quote'; diff --git a/lib/data/models/task_model.dart b/lib/data/models/task_model.dart index dccdedca6..1b4f36295 100644 --- a/lib/data/models/task_model.dart +++ b/lib/data/models/task_model.dart @@ -287,6 +287,9 @@ abstract class TaskTime implements Built { ); } + double calculateAmount(double taskRate) => + taskRate * round(duration.inSeconds / 3600, 3); + static Serializer get serializer => _$taskTimeSerializer; } @@ -350,7 +353,7 @@ abstract class TaskEntity extends Object TaskEntity stop() { final times = getTaskTimes(); - final taskTime = times.last!.stop; + final taskTime = times.last.stop; return updateTaskTime(taskTime, times.length - 1); } @@ -373,7 +376,7 @@ abstract class TaskEntity extends Object bool isValid = true; times.forEach((time) { - final startDate = time!.startDate; + final startDate = time.startDate; final endDate = time.endDate; if (time.isRunning) { @@ -404,7 +407,7 @@ abstract class TaskEntity extends Object int counter = 0; times.forEach((time) { - final startDate = time!.startDate; + final startDate = time.startDate; final endDate = time.endDate; if (time.isRunning) { @@ -434,7 +437,7 @@ abstract class TaskEntity extends Object return false; } - return taskTimes.any((taskTime) => taskTime!.isRunning); + return taskTimes.any((taskTime) => taskTime.isRunning); } bool isBetween(String? startDate, String? endDate) { @@ -445,16 +448,16 @@ abstract class TaskEntity extends Object } final taskStartDate = - convertDateTimeToSqlDate(taskTimes.first!.startDate!.toLocal()); + convertDateTimeToSqlDate(taskTimes.first.startDate!.toLocal()); if (startDate!.compareTo(taskStartDate) <= 0 && endDate!.compareTo(taskStartDate) >= 0) { return true; } - final completedTimes = taskTimes.where((element) => !element!.isRunning); + final completedTimes = taskTimes.where((element) => !element.isRunning); if (completedTimes.isNotEmpty) { - final lastTaskTime = completedTimes.last!; + final lastTaskTime = completedTimes.last; final taskEndDate = convertDateTimeToSqlDate(lastTaskTime.endDate!.toLocal()); @@ -504,8 +507,8 @@ abstract class TaskEntity extends Object return last[1].round(); } - List getTaskTimes({bool sort = true}) { - final List details = []; + List getTaskTimes({bool sort = true}) { + final List details = []; if (timeLog.isEmpty) { return details; @@ -541,8 +544,8 @@ abstract class TaskEntity extends Object }); if (sort) { - details.sort( - (timeA, timeB) => timeA!.startDate!.compareTo(timeB!.startDate!)); + details + .sort((timeA, timeB) => timeA.startDate!.compareTo(timeB.startDate!)); } return details; @@ -588,8 +591,8 @@ abstract class TaskEntity extends Object int seconds = 0; getTaskTimes().forEach((taskTime) { - if (!onlyBillable || taskTime!.isBillable) { - seconds += taskTime!.duration.inSeconds; + if (!onlyBillable || taskTime.isBillable) { + seconds += taskTime.duration.inSeconds; } }); diff --git a/lib/redux/dashboard/dashboard_selectors.dart b/lib/redux/dashboard/dashboard_selectors.dart index fd4d4944d..d68307791 100644 --- a/lib/redux/dashboard/dashboard_selectors.dart +++ b/lib/redux/dashboard/dashboard_selectors.dart @@ -688,7 +688,7 @@ List chartTasks( // skip it } else { task.getTaskTimes().forEach((taskTime) { - taskTime!.getParts().forEach((date, duration) { + taskTime.getParts().forEach((date, duration) { if (settings.groupBy == kReportGroupYear) { date = date.substring(0, 4) + '-01-01'; } else if (settings.groupBy == kReportGroupMonth) { diff --git a/lib/redux/project/project_selectors.dart b/lib/redux/project/project_selectors.dart index adc8f173b..be0141251 100644 --- a/lib/redux/project/project_selectors.dart +++ b/lib/redux/project/project_selectors.dart @@ -43,8 +43,8 @@ List convertProjectToInvoiceItem({ final taskTimesA = taskA!.getTaskTimes(); final taskTimesB = taskB!.getTaskTimes(); - final taskADate = taskTimesA.isEmpty ? null : taskTimesA.first!.startDate; - final taskBDate = taskTimesB.isEmpty ? null : taskTimesB.first!.startDate; + final taskADate = taskTimesA.isEmpty ? null : taskTimesA.first.startDate; + final taskBDate = taskTimesB.isEmpty ? null : taskTimesB.first.startDate; if (taskADate == null) { return 1; diff --git a/lib/redux/task/task_actions.dart b/lib/redux/task/task_actions.dart index 50fb3ea2d..cba789d69 100644 --- a/lib/redux/task/task_actions.dart +++ b/lib/redux/task/task_actions.dart @@ -420,10 +420,10 @@ void handleTaskAction( final taskBTimes = taskBEntity.getTaskTimes(); final taskADate = taskATimes.isEmpty ? convertTimestampToDate(taskA.createdAt) - : taskATimes.first!.startDate!; + : taskATimes.first.startDate!; final taskBDate = taskBTimes.isEmpty ? convertTimestampToDate(taskB.createdAt) - : taskBTimes.first!.startDate!; + : taskBTimes.first.startDate!; return taskADate.compareTo(taskBDate); }); diff --git a/lib/redux/task/task_selectors.dart b/lib/redux/task/task_selectors.dart index 7c662d7d2..39c5cfc3c 100644 --- a/lib/redux/task/task_selectors.dart +++ b/lib/redux/task/task_selectors.dart @@ -58,9 +58,9 @@ InvoiceItemEntity convertTaskToInvoiceItem({ task .getTaskTimes() .where((time) => - time!.startDate != null && time.endDate != null && time.isBillable) + time.startDate != null && time.endDate != null && time.isBillable) .forEach((time) { - final hours = round(time!.duration.inSeconds / 3600, 3); + final hours = round(time.duration.inSeconds / 3600, 3); final hoursStr = hours == 1 ? ' • 1 ${localization.hour}' : ' • $hours ${localization.hours}'; diff --git a/lib/ui/reports/reports_screen.dart b/lib/ui/reports/reports_screen.dart index a30bf0478..1eca0d777 100644 --- a/lib/ui/reports/reports_screen.dart +++ b/lib/ui/reports/reports_screen.dart @@ -119,7 +119,10 @@ class ReportsScreen extends StatelessWidget { ], kReportProduct, kReportProfitAndLoss, - kReportTask, + if (state.company.isModuleEnabled(EntityType.task)) ...[ + kReportTask, + kReportTaskItem, + ], if (state.company.isModuleEnabled(EntityType.vendor)) ...[ kReportVendor, if (state.company.isModuleEnabled(EntityType.purchaseOrder)) diff --git a/lib/ui/reports/reports_screen_vm.dart b/lib/ui/reports/reports_screen_vm.dart index 8df0d9bef..1cac3fb70 100644 --- a/lib/ui/reports/reports_screen_vm.dart +++ b/lib/ui/reports/reports_screen_vm.dart @@ -16,6 +16,7 @@ import 'package:invoiceninja_flutter/ui/reports/purchase_order_item_report.dart' import 'package:invoiceninja_flutter/ui/reports/purchase_order_report.dart'; import 'package:invoiceninja_flutter/ui/reports/recurring_expense_report.dart'; import 'package:invoiceninja_flutter/ui/reports/recurring_invoice_report.dart'; +import 'package:invoiceninja_flutter/ui/reports/task_item_report.dart'; import 'package:invoiceninja_flutter/ui/reports/transaction_report.dart'; import 'package:invoiceninja_flutter/ui/reports/vendor_report.dart'; import 'package:invoiceninja_flutter/utils/files.dart'; @@ -214,6 +215,20 @@ class ReportsScreenVM { state.staticState, ); break; + case kReportTaskItem: + reportResult = memoizedTaskItemReport( + state.userCompany, + state.uiState.reportsUIState, + state.taskState.map, + state.invoiceState.map, + state.groupState.map, + state.clientState.map, + state.taskStatusState.map, + state.userState.map, + state.projectState.map, + state.staticState, + ); + break; case kReportQuote: reportResult = memoizedQuoteReport( state.userCompany, diff --git a/lib/ui/reports/task_item_report.dart b/lib/ui/reports/task_item_report.dart new file mode 100644 index 000000000..ee44727cc --- /dev/null +++ b/lib/ui/reports/task_item_report.dart @@ -0,0 +1,310 @@ +// Package imports: +import 'package:built_collection/built_collection.dart'; +import 'package:collection/collection.dart' show IterableNullableExtension; +import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:invoiceninja_flutter/redux/reports/reports_selectors.dart'; +import 'package:memoize/memoize.dart'; + +// Project imports: +import 'package:invoiceninja_flutter/constants.dart'; +import 'package:invoiceninja_flutter/data/models/group_model.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/reports/reports_state.dart'; +import 'package:invoiceninja_flutter/redux/static/static_state.dart'; +import 'package:invoiceninja_flutter/redux/task/task_selectors.dart'; +import 'package:invoiceninja_flutter/ui/reports/reports_screen.dart'; +import 'package:invoiceninja_flutter/utils/enums.dart'; +import 'package:invoiceninja_flutter/utils/formatting.dart'; + +enum TaskItemReportFields { + number, + id, + rate, + calculated_rate, + start_time, + end_time, + duration, + description, + item_description, + invoice, + invoice_date, + invoice_due_date, + project, + client, + client_balance, + client_address1, + client_address2, + client_shipping_address1, + client_shipping_address2, + task1, + task2, + task3, + task4, + status, + assigned_to, + created_by, + amount, + record_state, +} + +var memoizedTaskItemReport = memo10(( + UserCompanyEntity? userCompany, + ReportsUIState reportsUIState, + BuiltMap taskMap, + BuiltMap invoiceMap, + BuiltMap groupMap, + BuiltMap clientMap, + BuiltMap taskStatusMap, + BuiltMap userMap, + BuiltMap projectMap, + StaticState staticState, +) => + taskItemReport( + userCompany!, + reportsUIState, + taskMap, + invoiceMap, + groupMap, + clientMap, + taskStatusMap, + userMap, + projectMap, + staticState, + )); + +ReportResult taskItemReport( + UserCompanyEntity userCompany, + ReportsUIState reportsUIState, + BuiltMap taskMap, + BuiltMap invoiceMap, + BuiltMap groupMap, + BuiltMap clientMap, + BuiltMap taskStatusMap, + BuiltMap userMap, + BuiltMap projectMap, + StaticState staticState, +) { + final List> data = []; + final List entities = []; + BuiltList columns; + + final reportSettings = userCompany.settings.reportSettings; + final taskReportSettings = reportSettings.containsKey(kReportTaskItem) + ? reportSettings[kReportTaskItem]! + : ReportSettingsEntity(); + + final defaultColumns = [ + TaskItemReportFields.number, + TaskItemReportFields.start_time, + TaskItemReportFields.end_time, + TaskItemReportFields.duration, + TaskItemReportFields.description, + TaskItemReportFields.client, + TaskItemReportFields.project, + TaskItemReportFields.invoice, + TaskItemReportFields.status, + ]; + + if (taskReportSettings.columns.isNotEmpty) { + columns = BuiltList(taskReportSettings.columns + .map((e) => EnumUtils.fromString(TaskItemReportFields.values, e)) + .whereNotNull() + .toList()); + } else { + columns = BuiltList(defaultColumns); + } + + for (var taskId in taskMap.keys) { + final task = taskMap[taskId]!; + final client = clientMap[task.clientId] ?? ClientEntity(); + final invoice = invoiceMap[task.invoiceId] ?? InvoiceEntity(); + final project = projectMap[task.projectId] ?? ProjectEntity(); + final group = groupMap[client.groupId] ?? GroupEntity(); + + if ((task.isDeleted! && !userCompany.company.reportIncludeDeleted) || + client.isDeleted!) { + continue; + } + + for (var taskItem in task.getTaskTimes()) { + bool skip = false; + final List row = []; + + for (var column in columns) { + dynamic value = ''; + + switch (column) { + case TaskItemReportFields.id: + value = task.id; + break; + case TaskItemReportFields.number: + value = task.number; + break; + case TaskItemReportFields.rate: + value = task.rate; + break; + case TaskItemReportFields.calculated_rate: + value = taskRateSelector( + company: userCompany.company, + project: project, + client: client, + task: task, + group: group, + ); + break; + case TaskItemReportFields.start_time: + if (taskItem.startDate == null) { + value = ''; + } else { + final timestamp = + (taskItem.startDate!.millisecondsSinceEpoch / 1000).floor(); + value = + timestamp > 0 ? convertTimestampToDateString(timestamp) : ''; + } + break; + case TaskItemReportFields.end_time: + if (taskItem.endDate == null) { + value = ''; + } else { + final timestamp = + (taskItem.endDate!.millisecondsSinceEpoch / 1000).floor(); + value = + timestamp > 0 ? convertTimestampToDateString(timestamp) : ''; + } + break; + case TaskItemReportFields.description: + value = task.description; + break; + case TaskItemReportFields.item_description: + value = taskItem.description; + break; + case TaskItemReportFields.invoice: + value = invoice.listDisplayName; + break; + case TaskItemReportFields.invoice_date: + value = invoice.isNew ? '' : invoice.date; + break; + case TaskItemReportFields.invoice_due_date: + value = invoice.isNew ? '' : invoice.dueDate; + break; + case TaskItemReportFields.duration: + value = taskItem.duration.inSeconds; + break; + case TaskItemReportFields.project: + value = projectMap[task.projectId]?.name ?? ''; + break; + case TaskItemReportFields.client: + value = clientMap[task.clientId]?.displayName ?? ''; + break; + case TaskItemReportFields.client_balance: + value = client.balance; + break; + case TaskItemReportFields.client_address1: + value = client.address1; + break; + case TaskItemReportFields.client_address2: + value = client.address2; + break; + case TaskItemReportFields.client_shipping_address1: + value = client.shippingAddress1; + break; + case TaskItemReportFields.client_shipping_address2: + value = client.shippingAddress2; + break; + case TaskItemReportFields.task1: + value = presentCustomField( + value: task.customValue1, + customFieldType: CustomFieldType.task1, + company: userCompany.company, + ); + break; + case TaskItemReportFields.task2: + value = presentCustomField( + value: task.customValue2, + customFieldType: CustomFieldType.task2, + company: userCompany.company, + ); + break; + case TaskItemReportFields.task3: + value = presentCustomField( + value: task.customValue3, + customFieldType: CustomFieldType.task3, + company: userCompany.company, + ); + break; + case TaskItemReportFields.task4: + value = presentCustomField( + value: task.customValue4, + customFieldType: CustomFieldType.task4, + company: userCompany.company, + ); + break; + case TaskItemReportFields.status: + value = taskStatusMap[task.statusId]?.name ?? ''; + break; + case TaskItemReportFields.assigned_to: + value = userMap[task.assignedUserId]?.listDisplayName ?? ''; + break; + case TaskItemReportFields.created_by: + value = userMap[task.createdUserId]?.listDisplayName ?? ''; + break; + case TaskItemReportFields.amount: + value = taskItem.calculateAmount( + taskRateSelector( + company: userCompany.company, + project: project, + client: client, + task: task, + group: group, + )!, + ); + break; + case TaskItemReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(task.entityState); + } + + if (!ReportResult.matchField( + value: value, + userCompany: userCompany, + reportsUIState: reportsUIState, + column: EnumUtils.parse(column), + )!) { + skip = true; + } + + if (column == TaskItemReportFields.duration) { + row.add(task.getReportDuration( + value: value, currencyId: client.currencyId)); + } else if (value.runtimeType == bool) { + row.add(task.getReportBool(value: value)); + } else if (value.runtimeType == double || value.runtimeType == int) { + row.add(task.getReportDouble( + value: value, currencyId: client.settings.currencyId)); + } else { + row.add(task.getReportString(value: value)); + } + } + + if (!skip) { + data.add(row); + entities.add(task); + } + } + } + + final selectedColumns = columns.map((item) => EnumUtils.parse(item)).toList(); + data.sort((rowA, rowB) => + sortReportTableRows(rowA, rowB, taskReportSettings, selectedColumns)!); + + return ReportResult( + allColumns: + TaskItemReportFields.values.map((e) => EnumUtils.parse(e)).toList(), + columns: selectedColumns, + defaultColumns: + defaultColumns.map((item) => EnumUtils.parse(item)).toList(), + data: data, + entities: entities, + ); +} diff --git a/lib/ui/task/edit/task_edit_desktop.dart b/lib/ui/task/edit/task_edit_desktop.dart index 0e852fa92..d9cc0dbe4 100644 --- a/lib/ui/task/edit/task_edit_desktop.dart +++ b/lib/ui/task/edit/task_edit_desktop.dart @@ -129,7 +129,7 @@ class _TaskEditDesktopState extends State { final showEndDate = company.showTaskEndDate; final taskTimes = task.getTaskTimes(sort: false); - if (!taskTimes.any((taskTime) => taskTime!.isEmpty)) { + if (!taskTimes.any((taskTime) => taskTime.isEmpty)) { taskTimes.add(TaskTime().rebuild((b) => b..startDate = null)); } @@ -333,15 +333,13 @@ class _TaskEditDesktopState extends State { ? localization.startDate : null, selectedDate: - taskTimes[index]!.startDate == null + taskTimes[index].startDate == null ? null : convertDateTimeToSqlDate( - taskTimes[index]! - .startDate! + taskTimes[index].startDate! .toLocal()), onSelected: (date, _) { - final taskTime = taskTimes[index]! - .copyWithStartDate(date, + final taskTime = taskTimes[index].copyWithStartDate(date, syncDates: !showEndDate); viewModel.onUpdatedTaskTime( taskTime, index); @@ -363,14 +361,13 @@ class _TaskEditDesktopState extends State { labelText: settings.showTaskItemDescription! ? localization.startTime : null, - selectedDateTime: taskTimes[index]!.startDate, + selectedDateTime: taskTimes[index].startDate, onSelected: (timeOfDay) { if (timeOfDay == null) { return; } - final taskTime = taskTimes[index]! - .copyWithStartTime(timeOfDay); + final taskTime = taskTimes[index].copyWithStartTime(timeOfDay); viewModel.onUpdatedTaskTime( taskTime, index); setState(() { @@ -393,15 +390,13 @@ class _TaskEditDesktopState extends State { ? localization.endDate : null, selectedDate: - taskTimes[index]!.endDate == null + taskTimes[index].endDate == null ? null : convertDateTimeToSqlDate( - taskTimes[index]! - .endDate! + taskTimes[index].endDate! .toLocal()), onSelected: (date, _) { - final taskTime = taskTimes[index]! - .copyWithEndDate(date); + final taskTime = taskTimes[index].copyWithEndDate(date); viewModel.onUpdatedTaskTime( taskTime, index); setState(() { @@ -422,15 +417,14 @@ class _TaskEditDesktopState extends State { labelText: settings.showTaskItemDescription! ? localization.endTime : null, - selectedDateTime: taskTimes[index]!.endDate, + selectedDateTime: taskTimes[index].endDate, isEndTime: true, onSelected: (timeOfDay) { if (timeOfDay == null) { return; } - final taskTime = taskTimes[index]! - .copyWithEndTime(timeOfDay); + final taskTime = taskTimes[index].copyWithEndTime(timeOfDay); viewModel.onUpdatedTaskTime( taskTime, index); setState(() { @@ -452,8 +446,7 @@ class _TaskEditDesktopState extends State { ? localization.duration : null, onSelected: (Duration duration) { - final taskTime = taskTimes[index]! - .copyWithDuration(duration); + final taskTime = taskTimes[index].copyWithDuration(duration); viewModel.onUpdatedTaskTime( taskTime, index); setState(() { @@ -462,10 +455,10 @@ class _TaskEditDesktopState extends State { }); }, selectedDuration: - (taskTimes[index]!.startDate == null || - taskTimes[index]!.endDate == null) + (taskTimes[index].startDate == null || + taskTimes[index].endDate == null) ? null - : taskTimes[index]!.duration, + : taskTimes[index].duration, ), ), ), @@ -477,7 +470,7 @@ class _TaskEditDesktopState extends State { const EdgeInsets.only(bottom: 16, right: 16), child: GrowableFormField( label: localization.description, - initialValue: taskTime!.description, + initialValue: taskTime.description, onChanged: (value) { viewModel.onUpdatedTaskTime( taskTime @@ -493,7 +486,7 @@ class _TaskEditDesktopState extends State { Padding( padding: const EdgeInsets.only(right: 8, left: 4), child: IconButton( - tooltip: taskTime!.isBillable + tooltip: taskTime.isBillable ? localization.billable : localization.notBillable, onPressed: taskTime.isEmpty @@ -514,7 +507,7 @@ class _TaskEditDesktopState extends State { tooltip: overlapping.contains(index) ? localization.invalidTime : localization.remove, - onPressed: taskTimes[index]!.isEmpty + onPressed: taskTimes[index].isEmpty ? null : () { confirmCallback( diff --git a/lib/ui/task/edit/task_edit_times.dart b/lib/ui/task/edit/task_edit_times.dart index 4e2b2a32f..46843daa9 100644 --- a/lib/ui/task/edit/task_edit_times.dart +++ b/lib/ui/task/edit/task_edit_times.dart @@ -42,7 +42,7 @@ class _TaskEditTimesState extends State { viewModel: viewModel, taskTime: taskTime, index: taskTimes.indexOf( - taskTimes.firstWhere((time) => time!.equalTo(taskTime!))), + taskTimes.firstWhere((time) => time.equalTo(taskTime!))), ); }); } diff --git a/lib/ui/task/edit/task_edit_vm.dart b/lib/ui/task/edit/task_edit_vm.dart index 8b406ed5b..ed162abd3 100644 --- a/lib/ui/task/edit/task_edit_vm.dart +++ b/lib/ui/task/edit/task_edit_vm.dart @@ -76,7 +76,7 @@ class TaskEditVM { final taskTimes = task.getTaskTimes(); store.dispatch(UpdateTaskTime( index: taskTimes.length - 1, - taskTime: taskTimes.firstWhere((time) => time!.isRunning)!.stop)); + taskTime: taskTimes.firstWhere((time) => time.isRunning).stop)); } else { store.dispatch(AddTaskTime(TaskTime())); } diff --git a/lib/ui/task/task_presenter.dart b/lib/ui/task/task_presenter.dart index 1c1422d84..fc031c4e4 100644 --- a/lib/ui/task/task_presenter.dart +++ b/lib/ui/task/task_presenter.dart @@ -96,9 +96,9 @@ class TaskPresenter extends EntityPresenter { final notes = []; task .getTaskTimes() - .where((time) => time!.startDate != null && time.endDate != null) + .where((time) => time.startDate != null && time.endDate != null) .forEach((time) { - final start = formatDate(time!.startDate!.toIso8601String(), context, + final start = formatDate(time.startDate!.toIso8601String(), context, showTime: true, showDate: true); final end = formatDate(time.endDate!.toIso8601String(), context, showTime: true, showDate: false); diff --git a/lib/ui/task/view/task_view_vm.dart b/lib/ui/task/view/task_view_vm.dart index 22f840e04..0692aaa44 100644 --- a/lib/ui/task/view/task_view_vm.dart +++ b/lib/ui/task/view/task_view_vm.dart @@ -108,7 +108,8 @@ class TaskViewVM { onEditPressed: (BuildContext context, [TaskTime? taskTime]) { editEntity( entity: task, - subIndex: task.getTaskTimes().indexOf(taskTime), + subIndex: + taskTime != null ? task.getTaskTimes().indexOf(taskTime) : 0, completer: snackBarCompleter( AppLocalization.of(context)!.updatedTask)); }, diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index 98324019e..df43c4791 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 + 'item_description': 'Item Description', + 'task_item': 'Task Item', 'record_state': 'Record State', 'last_login': 'Last Login', 'save_files_to_this_folder': 'Save files to this folder', @@ -109934,6 +109936,10 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]!['record_state'] ?? _localizedValues['en']!['record_state']!; + String get taskItem => + _localizedValues[localeCode]!['task_item'] ?? + _localizedValues['en']!['task_item']!; + // STARTER: lang field - do not remove comment String lookup(String? key) { From 5272305b312f4544523c1b96c577bf464781aa85 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 30 Oct 2023 21:23:32 +0200 Subject: [PATCH 024/138] Prevent duplicate error popups --- lib/ui/invoice/invoice_pdf.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index 89c3ff9b0..19b30188f 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -29,7 +29,6 @@ import 'package:invoiceninja_flutter/ui/app/forms/app_dropdown_button.dart'; import 'package:invoiceninja_flutter/ui/app/loading_indicator.dart'; import 'package:invoiceninja_flutter/ui/app/presenters/entity_presenter.dart'; import 'package:invoiceninja_flutter/ui/invoice/invoice_pdf_vm.dart'; -import 'package:invoiceninja_flutter/utils/dialogs.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart'; @@ -362,7 +361,6 @@ Future _loadPDF( errorMessage += response.body; } - showErrorDialog(message: errorMessage); throw errorMessage; } From ea9cc10dac4ea1fab246d7e07f1a0bfb2e2d407a Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 30 Oct 2023 21:23:41 +0200 Subject: [PATCH 025/138] Include more info in error popup --- lib/data/web_client.dart | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/data/web_client.dart b/lib/data/web_client.dart index 96a18fa78..a91cf1b84 100644 --- a/lib/data/web_client.dart +++ b/lib/data/web_client.dart @@ -252,7 +252,8 @@ void _checkResponse(String url, http.Response response) { final minClientVersion = response.headers['x-minimum-client-version']; if (response.statusCode >= 500) { - throw _parseError(response.statusCode, response.body); + throw _parseError( + response.statusCode, response.body, response.reasonPhrase); } else if (serverVersion == null) { throw 'Error: please check that Invoice Ninja v5 is installed on the server\n\nURL: $url\n\nResponse: ${response.body.length > 200 ? response.body.substring(0, 200) : response.body}\n\nHeaders: ${response.headers}}'; } else if (Version.parse(kClientVersion) < Version.parse(minClientVersion!)) { @@ -260,7 +261,8 @@ void _checkResponse(String url, http.Response response) { } else if (Version.parse(serverVersion) < Version.parse(kMinServerVersion)) { throw 'Error: server not supported, please update to the latest version [Current v$serverVersion < Minimum v$kMinServerVersion]'; } else if (response.statusCode >= 400) { - throw _parseError(response.statusCode, response.body); + throw _parseError( + response.statusCode, response.body, response.reasonPhrase); } } @@ -277,8 +279,12 @@ void _preCheck() { */ } -String _parseError(int code, String response) { - dynamic message = response; +String _parseError(int code, String response, String? reason) { + String message = ''; + + if ((reason ?? '').isNotEmpty) { + message += reason! + ' • '; + } if (response.contains('DOCTYPE html')) { return '$code: An error occurred'; From aa9f846de43595baed6abee46edf99fffe2819a8 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 30 Oct 2023 21:26:28 +0200 Subject: [PATCH 026/138] Include more info in error popup --- lib/data/web_client.dart | 2 +- lib/ui/invoice/invoice_pdf.dart | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/data/web_client.dart b/lib/data/web_client.dart index a91cf1b84..67f728483 100644 --- a/lib/data/web_client.dart +++ b/lib/data/web_client.dart @@ -293,7 +293,7 @@ String _parseError(int code, String response, String? reason) { try { final dynamic jsonResponse = json.decode(response); - message = jsonResponse['message'] ?? jsonResponse; + message += jsonResponse['message'] ?? jsonResponse; if (jsonResponse['errors'] != null && (jsonResponse['errors'] as Map).isNotEmpty) { diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index 19b30188f..4bc9caa61 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -347,8 +347,9 @@ Future _loadPDF( .get('${credential.url}$url', credential.token, rawResponse: true); } else { final invitation = invoice.invitations.first; - final url = invitation.downloadLink; - response = await WebClient().get(url, '', rawResponse: true); + final url = invitation.downloadLink + .substring(0, invitation.downloadLink.length - 30); + response = await WebClient().get(url, '', rawResponse: false); } if (response!.statusCode >= 400) { From 18362a6221d12add5f772225ea9b930bb1073fb5 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 30 Oct 2023 21:26:41 +0200 Subject: [PATCH 027/138] Include more info in error popup --- lib/ui/invoice/invoice_pdf.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index 4bc9caa61..19b30188f 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -347,9 +347,8 @@ Future _loadPDF( .get('${credential.url}$url', credential.token, rawResponse: true); } else { final invitation = invoice.invitations.first; - final url = invitation.downloadLink - .substring(0, invitation.downloadLink.length - 30); - response = await WebClient().get(url, '', rawResponse: false); + final url = invitation.downloadLink; + response = await WebClient().get(url, '', rawResponse: true); } if (response!.statusCode >= 400) { From 5f21d90340ff55f41f30ddc15b10b841382e1998 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 30 Oct 2023 21:42:34 +0200 Subject: [PATCH 028/138] Feature request: Report of not invoiced hours per client or in total #587 --- lib/ui/reports/task_item_report.dart | 5 +++++ lib/ui/reports/task_report.dart | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/lib/ui/reports/task_item_report.dart b/lib/ui/reports/task_item_report.dart index ee44727cc..1d5236912 100644 --- a/lib/ui/reports/task_item_report.dart +++ b/lib/ui/reports/task_item_report.dart @@ -46,6 +46,7 @@ enum TaskItemReportFields { created_by, amount, record_state, + is_invoiced, } var memoizedTaskItemReport = memo10(( @@ -263,6 +264,10 @@ ReportResult taskItemReport( case TaskItemReportFields.record_state: value = AppLocalization.of(navigatorKey.currentContext!)! .lookup(task.entityState); + break; + case TaskItemReportFields.is_invoiced: + value = task.isInvoiced; + break; } if (!ReportResult.matchField( diff --git a/lib/ui/reports/task_report.dart b/lib/ui/reports/task_report.dart index 0a0541a93..e915a39ff 100644 --- a/lib/ui/reports/task_report.dart +++ b/lib/ui/reports/task_report.dart @@ -46,6 +46,7 @@ enum TaskReportFields { created_by, amount, record_state, + is_invoiced, } var memoizedTaskReport = memo10(( @@ -253,6 +254,10 @@ ReportResult taskReport( case TaskReportFields.record_state: value = AppLocalization.of(navigatorKey.currentContext!)! .lookup(task.entityState); + break; + case TaskReportFields.is_invoiced: + value = task.isInvoiced; + break; } if (!ReportResult.matchField( From d3ec7a6769c2996e1a30e681427cbefcae07f3e8 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 30 Oct 2023 23:20:49 +0200 Subject: [PATCH 029/138] Feature Request: Report (Invoice) -> Group by Date (Quarterly #569 --- lib/constants.dart | 1 + lib/ui/reports/reports_screen.dart | 4 ++++ lib/ui/reports/reports_screen_vm.dart | 13 +++++++++++++ lib/utils/i18n.dart | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/lib/constants.dart b/lib/constants.dart index a083a0d9c..2722e3861 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -737,6 +737,7 @@ const String kTaxRegionAustralia = 'AU'; const String kReportGroupDay = 'day'; const String kReportGroupWeek = 'week'; const String kReportGroupMonth = 'month'; +const String kReportGroupQuarter = 'quarter'; const String kReportGroupYear = 'year'; const int kModuleRecurringInvoices = 1; diff --git a/lib/ui/reports/reports_screen.dart b/lib/ui/reports/reports_screen.dart index 1eca0d777..5b9ced148 100644 --- a/lib/ui/reports/reports_screen.dart +++ b/lib/ui/reports/reports_screen.dart @@ -190,6 +190,10 @@ class ReportsScreen extends StatelessWidget { child: Text(localization.month), value: kReportGroupMonth, ), + DropdownMenuItem( + child: Text(localization.quarter), + value: kReportGroupQuarter, + ), DropdownMenuItem( child: Text(localization.year), value: kReportGroupYear, diff --git a/lib/ui/reports/reports_screen_vm.dart b/lib/ui/reports/reports_screen_vm.dart index 1cac3fb70..58ef63657 100644 --- a/lib/ui/reports/reports_screen_vm.dart +++ b/lib/ui/reports/reports_screen_vm.dart @@ -652,6 +652,19 @@ GroupTotals calculateReportTotals({ group = group.substring(0, 4) + '-01-01'; } else if (reportState.subgroup == kReportGroupMonth) { group = group.substring(0, 7) + '-01'; + } else if (reportState.subgroup == kReportGroupQuarter) { + final parts = group.split('-'); + final month = parseInt(parts[1]) ?? 0; + group = parts[0] + '-'; + if (month <= 3) { + group += '01-01'; + } else if (month <= 6) { + group += '04-01'; + } else if (month <= 9) { + group += '07-01'; + } else { + group += '10-01'; + } } else if (reportState.subgroup == kReportGroupWeek) { final date = DateTime.parse(group); final dateWeek = diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index df43c4791..723bbbe9e 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -18,6 +18,7 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'quarter': 'Quarter', 'item_description': 'Item Description', 'task_item': 'Task Item', 'record_state': 'Record State', @@ -109940,6 +109941,11 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]!['task_item'] ?? _localizedValues['en']!['task_item']!; + String get quarter => + _localizedValues[localeCode]!['quarter'] ?? + _localizedValues['en']!['quarter']!; + + // STARTER: lang field - do not remove comment String lookup(String? key) { From 88b8ea7bacc9bc92db8c40f87e1e9d965620d087 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 30 Oct 2023 23:24:55 +0200 Subject: [PATCH 030/138] Feature Request: Report (Invoice) -> Group by Date (Quarterly #569 --- lib/ui/reports/reports_screen.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/ui/reports/reports_screen.dart b/lib/ui/reports/reports_screen.dart index 5b9ced148..15877ebd8 100644 --- a/lib/ui/reports/reports_screen.dart +++ b/lib/ui/reports/reports_screen.dart @@ -1397,6 +1397,9 @@ class ReportResult { customStartDate = group; if (reportState.subgroup == kReportGroupDay) { customEndDate = convertDateTimeToSqlDate(date); + } else if (reportState.subgroup == kReportGroupQuarter) { + customEndDate = + convertDateTimeToSqlDate(addDays(addMonths(date!, 3), -1)); } else if (reportState.subgroup == kReportGroupMonth) { customEndDate = convertDateTimeToSqlDate(addDays(addMonths(date!, 1), -1)); From a5bdbd73de16629c1ca0a66f5f90eab48d3a545f Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 31 Oct 2023 12:40:15 +0200 Subject: [PATCH 031/138] Flatpak --- .github/workflows/build.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9a4657c92..d4457fb99 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,19 +19,25 @@ jobs: sentry_url: ${{secrets.sentry_url}} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - uses: subosito/flutter-action@v1 + - name: Checkout code + uses: actions/checkout@v1 + + - name: Setup Flutter + uses: subosito/flutter-action@v1 with: flutter-version: '3.13.6' #channel: 'stable' + - name: Install Sentry run: | curl -sL https://sentry.io/get-cli/ | bash - - name: Setup Flutter + + - name: Check Flutter run: | flutter doctor -v flutter pub get flutter config --enable-web + - name: Prepare App run: | cp lib/.env.dart.example lib/.env.dart @@ -39,6 +45,7 @@ jobs: echo "const FLUTTER_VERSION = const " > lib/flutter_version.dart flutter --version --machine >> lib/flutter_version.dart echo ";" >> lib/flutter_version.dart + - name: Build Hosted App run: | #export SENTRY_RELEASE=$(sentry-cli releases propose-version) @@ -70,6 +77,7 @@ jobs: #sentry-cli --auth-token ${{secrets.sentry_auth_token}} --url ${{secrets.sentry_url}} releases --org ${{secrets.sentry_org}} finalize $SENTRY_RELEASE #sentry-cli --auth-token ${{secrets.sentry_auth_token}} --url ${{secrets.sentry_url}} releases --org ${{secrets.sentry_org}} deploys $SENTRY_RELEASE new -e production + - name: Build Profile App run: | flutter build web --profile @@ -83,6 +91,7 @@ jobs: git commit -m 'Admin Portal - Profile' git push cd .. + - name: Build Selfhosted App run: | cp lib/utils/oauth.dart.foss lib/utils/oauth.dart From dc490a9aebe50536f4babb24c0a52cd35d795a80 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 31 Oct 2023 12:45:03 +0200 Subject: [PATCH 032/138] Flatpak --- .github/workflows/flatpak.yml | 85 +++++++++++++++++++++++ flatpak/com.invoiceninja.app.desktop | 12 ++++ flatpak/com.invoiceninja.app.metainfo.xml | 43 ++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 .github/workflows/flatpak.yml create mode 100644 flatpak/com.invoiceninja.app.desktop create mode 100644 flatpak/com.invoiceninja.app.metainfo.xml diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml new file mode 100644 index 000000000..af6632179 --- /dev/null +++ b/.github/workflows/flatpak.yml @@ -0,0 +1,85 @@ +name: Build Flatpak + +on: + workflow_dispatch: + #push: + # branches: + # - master + +env: + project-id: com.invoiceninja.app + +jobs: + build-flutter-app: + name: Build Flutter app + env: + api_secret: ${{secrets.api_secret}} + runs-on: ubuntu-20.04 + steps: + - name: Checkout code + uses: actions/checkout@v3 + + #- name: Install Flutter dependencies + # run: | + # sudo apt-get update + # sudo apt-get install -y libgtk-3-dev libx11-dev pkg-config cmake ninja-build libblkid-dev + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.13.6' + #channel: 'stable' + + - name: Init Flutter + run: | + flutter doctor -v + flutter pub get + + - name: Prepare App + run: | + cp lib/.env.dart.example lib/.env.dart + sed -i 's/secret/${{secrets.api_secret}}/g' lib/.env.dart + echo "const FLUTTER_VERSION = const " > lib/flutter_version.dart + flutter --version --machine >> lib/flutter_version.dart + echo ";" >> lib/flutter_version.dart + + - name: Build Flutter linux version + #working-directory: ${{ github.workspace }}/counter_app + run: | + archiveName=Invoice-Ninja-Linux-Portable.tar.gz + baseDir=$(pwd) + + flutter build linux + + cd build/linux/x64/release/bundle || exit + tar -czaf $archiveName ./* + mv $archiveName "$baseDir"/ + + - name: Upload app archive to workflow + uses: actions/upload-artifact@v3 + with: + name: Invoice-Ninja-Archive + path: Invoice-Ninja-Linux-Portable.tar.gz + +# build-flatpak: +# name: Build flatpak +# needs: build-flutter-app +# runs-on: ubuntu-latest +# container: +# image: bilelmoussaoui/flatpak-github-actions:freedesktop-22.08 +# options: --privileged +# steps: +# - name: Checkout code +# uses: actions/checkout@v3 +# +# - name: Build .flatpak +# uses: bilelmoussaoui/flatpak-github-actions/flatpak-builder@v5 +# with: +# bundle: FlutterApp.flatpak +# manifest-path: flathub_repo/com.example.FlutterApp.yml +# +# - name: Upload .flatpak artifact to workflow +# uses: actions/upload-artifact@v3 +# with: +# name: Flatpak artifact +# path: FlutterApp.flatpak \ No newline at end of file diff --git a/flatpak/com.invoiceninja.app.desktop b/flatpak/com.invoiceninja.app.desktop new file mode 100644 index 000000000..1b9f9a99f --- /dev/null +++ b/flatpak/com.invoiceninja.app.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Version=1.0 +Type=Application + +Name=Invoice Ninja +Comment=An example app that counts +Categories=Utility;Amusement; + +Icon=com.example.FlutterApp +Exec=flutter_flatpak_example +Terminal=false +StartupWMClass=flutter_flatpak_example \ No newline at end of file diff --git a/flatpak/com.invoiceninja.app.metainfo.xml b/flatpak/com.invoiceninja.app.metainfo.xml new file mode 100644 index 000000000..8a4632634 --- /dev/null +++ b/flatpak/com.invoiceninja.app.metainfo.xml @@ -0,0 +1,43 @@ + + + + com.example.FlutterApp + Flutter App + An example app that counts + Kristen McWilliam + https://github.com/Merrit/flutter_flatpak_example + MIT + MIT + + pointing + keyboard + touch + + +

Fast and beautiful counting app. For all your counting needs!

+

Features

+
    +
  • Counts!
  • +
  • Cooks!
  • +
  • Cleans!
  • +
  • Comets!
  • +
  • Dinosaurs!
  • +
  • .. Burritos?
  • +
  • Burritos!!
  • +
  • +
+
+ com.example.FlutterApp.desktop + + + https://raw.githubusercontent.com/Merrit/flutter_flatpak_example/main/screenshots/screenshot.png + + + + + + +
\ No newline at end of file From a5de998d123b1746e10de1eb46f155729597c1f6 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 31 Oct 2023 12:49:08 +0200 Subject: [PATCH 033/138] Flatpak --- .github/workflows/flatpak.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index af6632179..4f31fa439 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -19,10 +19,10 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - #- name: Install Flutter dependencies - # run: | - # sudo apt-get update - # sudo apt-get install -y libgtk-3-dev libx11-dev pkg-config cmake ninja-build libblkid-dev + - name: Install Flutter dependencies + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libx11-dev pkg-config cmake ninja-build libblkid-dev - name: Setup Flutter uses: subosito/flutter-action@v2 From b592fb821d0d553cb1b9bdc5b31833d1c7e64a06 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 08:45:46 +0200 Subject: [PATCH 034/138] Flatpak --- .github/workflows/build.yml | 2 +- flatpak/com.invoiceninja.app.desktop | 10 +++---- flatpak/com.invoiceninja.app.metainfo.xml | 35 ++++++++--------------- snap/snapcraft.yaml | 2 +- 4 files changed, 19 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d4457fb99..8553a0bc6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v1 - name: Setup Flutter - uses: subosito/flutter-action@v1 + uses: subosito/flutter-action@v2 with: flutter-version: '3.13.6' #channel: 'stable' diff --git a/flatpak/com.invoiceninja.app.desktop b/flatpak/com.invoiceninja.app.desktop index 1b9f9a99f..2b0704d5a 100644 --- a/flatpak/com.invoiceninja.app.desktop +++ b/flatpak/com.invoiceninja.app.desktop @@ -3,10 +3,10 @@ Version=1.0 Type=Application Name=Invoice Ninja -Comment=An example app that counts -Categories=Utility;Amusement; +Comment=Create invoices, accept payments, track expenses & time tasks +Categories=Productivity; -Icon=com.example.FlutterApp -Exec=flutter_flatpak_example +Icon=com.invoiceninja.app +Exec=invoiceninja_client Terminal=false -StartupWMClass=flutter_flatpak_example \ No newline at end of file +StartupWMClass=invoiceninja_client \ No newline at end of file diff --git a/flatpak/com.invoiceninja.app.metainfo.xml b/flatpak/com.invoiceninja.app.metainfo.xml index 8a4632634..12a985f5b 100644 --- a/flatpak/com.invoiceninja.app.metainfo.xml +++ b/flatpak/com.invoiceninja.app.metainfo.xml @@ -4,40 +4,29 @@ https://www.freedesktop.org/software/appstream/metainfocreator/#/ --> - com.example.FlutterApp - Flutter App - An example app that counts - Kristen McWilliam - https://github.com/Merrit/flutter_flatpak_example - MIT - MIT + com.invoiceninja.app + Invoice Ninja + Create invoices, accept payments, track expenses & time tasks + Hillel Coren + https://invoiceninja.com + AAL + AAL pointing keyboard touch -

Fast and beautiful counting app. For all your counting needs!

-

Features

-
    -
  • Counts!
  • -
  • Cooks!
  • -
  • Cleans!
  • -
  • Comets!
  • -
  • Dinosaurs!
  • -
  • .. Burritos?
  • -
  • Burritos!!
  • -
  • -
+

Create invoices, accept payments, track expenses & time tasks

- com.example.FlutterApp.desktop + com.invoiceninja.app.desktop - https://raw.githubusercontent.com/Merrit/flutter_flatpak_example/main/screenshots/screenshot.png + https://github.com/invoiceninja/flutter-mobile/blob/master/samples/screenshots/1.png - + - +
\ No newline at end of file diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 35bbce8a1..4d1c820b2 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,6 +1,6 @@ name: invoiceninja version: '5.0.127' -summary: Create invoices, accept payments, track expenses & time-tasks +summary: Create invoices, accept payments, track expenses & time tasks description: "### Note: if the app fails to run using `snap run invoiceninja` it may help to run `/snap/invoiceninja/current/bin/invoiceninja` instead Create. Send. Get Paid. From 2a125a366659e66cf60b0225ecd05a9f88b9fc1f Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 12:02:23 +0200 Subject: [PATCH 035/138] Update app version --- .github/workflows/build.yml | 5 +++++ .github/workflows/flatpak.yml | 27 +++++++++++++++++++++++++-- assets/images/logo.svg | 2 ++ lib/constants.dart | 2 +- pubspec.foss.yaml | 2 +- pubspec.yaml | 2 +- snap/snapcraft.yaml | 2 +- 7 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 assets/images/logo.svg diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8553a0bc6..c68433186 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,11 @@ on: push: branches: - master + +concurrency: + group: ci-release-${{ github.ref }}-1 + cancel-in-progress: true + jobs: build-main: name: Build Web - MAIN diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 4f31fa439..61e8310f0 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -6,6 +6,10 @@ on: # branches: # - master +concurrency: + group: ci-release-${{ github.ref }}-1 + cancel-in-progress: true + env: project-id: com.invoiceninja.app @@ -14,6 +18,7 @@ jobs: name: Build Flutter app env: api_secret: ${{secrets.api_secret}} + commit_secret: ${{secrets.commit_secret}} runs-on: ubuntu-20.04 steps: - name: Checkout code @@ -58,8 +63,26 @@ jobs: - name: Upload app archive to workflow uses: actions/upload-artifact@v3 with: - name: Invoice-Ninja-Archive - path: Invoice-Ninja-Linux-Portable.tar.gz + #name: Invoice-Ninja-Archive + #path: Invoice-Ninja-Linux-Portable.tar.gz + name: linux-artifacts + path: output/* + + - name: Download artifacts + uses: actions/download-artifact@v3 + with: + path: artifacts + + - name: Create Draft Release & Upload artifacts + uses: marvinpinto/action-automatic-releases@v1.2.1 + with: + repo_token: "${{secrets.commit_secret}}" + draft: true + prerelease: false + title: "Latest Release" + automatic_release_tag: "v5.0.128" + files: | + ${{ github.workspace }}/artifacts/linux-artifacts/* # build-flatpak: # name: Build flatpak diff --git a/assets/images/logo.svg b/assets/images/logo.svg new file mode 100644 index 000000000..8e838c8f8 --- /dev/null +++ b/assets/images/logo.svg @@ -0,0 +1,2 @@ + +Invoice Ninja icon \ No newline at end of file diff --git a/lib/constants.dart b/lib/constants.dart index 2722e3861..011fbf055 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -4,7 +4,7 @@ class Constants { } // TODO remove version once #46609 is fixed -const String kClientVersion = '5.0.127'; +const String kClientVersion = '5.0.128'; const String kMinServerVersion = '5.0.4'; const String kAppName = 'Invoice Ninja'; diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index 7acc0400d..450d2b66c 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.127+127 +version: 5.0.128+128 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/pubspec.yaml b/pubspec.yaml index 209d8d69e..2805ca5ca 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.127+127 +version: 5.0.128+128 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 4d1c820b2..20900ff0a 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: invoiceninja -version: '5.0.127' +version: '5.0.128' summary: Create invoices, accept payments, track expenses & time tasks description: "### Note: if the app fails to run using `snap run invoiceninja` it may help to run `/snap/invoiceninja/current/bin/invoiceninja` instead From db95c80a6aed4a61e43771137b4b1a0bcc9fc273 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 12:06:06 +0200 Subject: [PATCH 036/138] Flatpak --- .github/workflows/build.yml | 6 +++--- .github/workflows/flatpak.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c68433186..09725687f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,9 +5,9 @@ on: branches: - master -concurrency: - group: ci-release-${{ github.ref }}-1 - cancel-in-progress: true +#concurrency: +# group: ci-release-${{ github.ref }}-1 +# cancel-in-progress: true jobs: build-main: diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 61e8310f0..62a01eae3 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -6,9 +6,9 @@ on: # branches: # - master -concurrency: - group: ci-release-${{ github.ref }}-1 - cancel-in-progress: true +#concurrency: +# group: ci-release-${{ github.ref }}-1 +# cancel-in-progress: true env: project-id: com.invoiceninja.app From bd38ab1d13ab121077a85c8abead20251b446062 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 12:17:58 +0200 Subject: [PATCH 037/138] Flatpak --- .github/workflows/flatpak.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 62a01eae3..af560ceb4 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -63,10 +63,10 @@ jobs: - name: Upload app archive to workflow uses: actions/upload-artifact@v3 with: - #name: Invoice-Ninja-Archive - #path: Invoice-Ninja-Linux-Portable.tar.gz - name: linux-artifacts - path: output/* + name: Invoice-Ninja-Archive + path: Invoice-Ninja-Linux-Portable.tar.gz + #name: linux-artifacts + #path: output/* - name: Download artifacts uses: actions/download-artifact@v3 @@ -82,7 +82,7 @@ jobs: title: "Latest Release" automatic_release_tag: "v5.0.128" files: | - ${{ github.workspace }}/artifacts/linux-artifacts/* + ${{ github.workspace }}/artifacts/* # build-flatpak: # name: Build flatpak From fedc0eb13f435f52660a6877730c04ac8910df84 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 12:25:28 +0200 Subject: [PATCH 038/138] Flatpak --- .github/workflows/flatpak.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index af560ceb4..648e15617 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -82,7 +82,7 @@ jobs: title: "Latest Release" automatic_release_tag: "v5.0.128" files: | - ${{ github.workspace }}/artifacts/* + ${{ github.workspace }}/artifacts/Invoice-Ninja-Archive # build-flatpak: # name: Build flatpak From 39bc3c84c91da51d0f8711c6f0cdffffd2bcab2f Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 12:38:08 +0200 Subject: [PATCH 039/138] Flatpak --- .github/workflows/flatpak.yml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 648e15617..5c9fb7365 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -17,8 +17,8 @@ jobs: build-flutter-app: name: Build Flutter app env: - api_secret: ${{secrets.api_secret}} - commit_secret: ${{secrets.commit_secret}} + api_secret: ${{ secrets.api_secret }} + commit_secret: ${{ secrets.commit_secret }} runs-on: ubuntu-20.04 steps: - name: Checkout code @@ -43,13 +43,12 @@ jobs: - name: Prepare App run: | cp lib/.env.dart.example lib/.env.dart - sed -i 's/secret/${{secrets.api_secret}}/g' lib/.env.dart + sed -i 's/secret/${{ secrets.api_secret }}/g' lib/.env.dart echo "const FLUTTER_VERSION = const " > lib/flutter_version.dart flutter --version --machine >> lib/flutter_version.dart echo ";" >> lib/flutter_version.dart - name: Build Flutter linux version - #working-directory: ${{ github.workspace }}/counter_app run: | archiveName=Invoice-Ninja-Linux-Portable.tar.gz baseDir=$(pwd) @@ -65,19 +64,17 @@ jobs: with: name: Invoice-Ninja-Archive path: Invoice-Ninja-Linux-Portable.tar.gz - #name: linux-artifacts - #path: output/* - name: Download artifacts uses: actions/download-artifact@v3 with: path: artifacts - - name: Create Draft Release & Upload artifacts + - name: Create Release uses: marvinpinto/action-automatic-releases@v1.2.1 with: - repo_token: "${{secrets.commit_secret}}" - draft: true + repo_token: "${{ secrets.commit_secret }}" + draft: false prerelease: false title: "Latest Release" automatic_release_tag: "v5.0.128" From 500548c7f763f05cf41b32d8f06656cd9e7147fc Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 12:51:06 +0200 Subject: [PATCH 040/138] Flatpak --- flatpak/com.invoiceninja.app.metainfo.xml | 24 +++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/flatpak/com.invoiceninja.app.metainfo.xml b/flatpak/com.invoiceninja.app.metainfo.xml index 12a985f5b..2959d1ae3 100644 --- a/flatpak/com.invoiceninja.app.metainfo.xml +++ b/flatpak/com.invoiceninja.app.metainfo.xml @@ -17,7 +17,27 @@ touch -

Create invoices, accept payments, track expenses & time tasks

+

Create. Send. Get Paid.

+

Invoice Ninja is a leading source-code available platform for SMB’s to invoice, accept payments, track expenses & time billable-tasks. Designed for freelancers and small to medium size businesses, Invoice Ninja is a suite of apps to help you get paid.

+

+ • Incredibly easy to use
+ Invoice Ninja was built to serve freelancers and business owners with a complete suite of invoicing & payment tools to advance your business. +

+

+ • Invoicing & Payments
+ Every feature is geared towards accurate and secure invoicing and getting you paid. With Invoice Ninja you can send beautiful branded invoices with minimum of effort and maximum professionalism. +

+

+ • Time Tracker & Projects
+ Create projects and individual tasks per project. When done, simply “Send task to invoice” and all details will be sent ready for your clients to pay! +

+

+ • Track Vendors & Expenses
+ With Invoice Ninja, all your earnings, expenses, clients and vendors are stored and managed in one system. Categorize your vendors & re-invoice expenses to clients, or simply run expense reports. +

+

+ All of these features combine to help you receive the money you deserve and reduce the amount of time you spend on repetitive invoicing tasks. Spend less time on paperwork and more time at your craft. +

com.invoiceninja.app.desktop @@ -27,6 +47,6 @@ - + \ No newline at end of file From 78f2b17793bbfdf90a482520db83ab952ec31340 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 13:26:53 +0200 Subject: [PATCH 041/138] Flatpak --- assets/images/{logo.svg => com.invoiceninja.app.svg} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename assets/images/{logo.svg => com.invoiceninja.app.svg} (100%) diff --git a/assets/images/logo.svg b/assets/images/com.invoiceninja.app.svg similarity index 100% rename from assets/images/logo.svg rename to assets/images/com.invoiceninja.app.svg From d2b1f3760713c5c983cf233fdc461419c6ee65cc Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 13:28:25 +0200 Subject: [PATCH 042/138] Flatpak --- samples/screenshots/5.png | Bin 0 -> 171730 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 samples/screenshots/5.png diff --git a/samples/screenshots/5.png b/samples/screenshots/5.png new file mode 100644 index 0000000000000000000000000000000000000000..a8989482815430d458791ed068a7b9bfb9c3c9cf GIT binary patch literal 171730 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

Ogti5fAOx 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

+RhoHYSbe#ew3{ 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 literal 0 HcmV?d00001 From 137c225c71a7056f5fe2102db2ec908e7f01706f Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 13:29:22 +0200 Subject: [PATCH 043/138] Flatpak --- flatpak/com.invoiceninja.app.metainfo.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flatpak/com.invoiceninja.app.metainfo.xml b/flatpak/com.invoiceninja.app.metainfo.xml index 2959d1ae3..de22643c3 100644 --- a/flatpak/com.invoiceninja.app.metainfo.xml +++ b/flatpak/com.invoiceninja.app.metainfo.xml @@ -42,7 +42,7 @@ com.invoiceninja.app.desktop - https://github.com/invoiceninja/flutter-mobile/blob/master/samples/screenshots/1.png + https://github.com/invoiceninja/admin-portal/blob/master/samples/screenshots/5.png From 9235746ba2c16548be1134e8bc96e102bd43053d Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 13:38:58 +0200 Subject: [PATCH 044/138] Flatpak --- .github/workflows/flatpak.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 5c9fb7365..68bbbe999 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -59,6 +59,16 @@ jobs: tar -czaf $archiveName ./* mv $archiveName "$baseDir"/ + - uses: MCJack123/ghaction-generate-release-hashes@v3 + with: + hash-type: sha256 + file-name: hashes.txt + + - uses: actions/upload-artifact@v2 + with: + name: Asset Hashes + path: hashes.txt + - name: Upload app archive to workflow uses: actions/upload-artifact@v3 with: @@ -80,6 +90,7 @@ jobs: automatic_release_tag: "v5.0.128" files: | ${{ github.workspace }}/artifacts/Invoice-Ninja-Archive + ${{ github.workspace }}/artifacts/hashes.txt # build-flatpak: # name: Build flatpak From 7358d9e6f734bd3fc5c62fb523063e19a75e7425 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 15:57:12 +0200 Subject: [PATCH 045/138] Flatpak --- .github/workflows/flatpak.yml | 13 +------------ .github/workflows/hashes.yml | 28 ++++++++++++++++++++++++++++ bump_version.sh | 16 ++++++++++++++++ 3 files changed, 45 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/hashes.yml create mode 100644 bump_version.sh diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 68bbbe999..bf79ac5aa 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -15,7 +15,7 @@ env: jobs: build-flutter-app: - name: Build Flutter app + name: Build Flutter env: api_secret: ${{ secrets.api_secret }} commit_secret: ${{ secrets.commit_secret }} @@ -59,16 +59,6 @@ jobs: tar -czaf $archiveName ./* mv $archiveName "$baseDir"/ - - uses: MCJack123/ghaction-generate-release-hashes@v3 - with: - hash-type: sha256 - file-name: hashes.txt - - - uses: actions/upload-artifact@v2 - with: - name: Asset Hashes - path: hashes.txt - - name: Upload app archive to workflow uses: actions/upload-artifact@v3 with: @@ -90,7 +80,6 @@ jobs: automatic_release_tag: "v5.0.128" files: | ${{ github.workspace }}/artifacts/Invoice-Ninja-Archive - ${{ github.workspace }}/artifacts/hashes.txt # build-flatpak: # name: Build flatpak diff --git a/.github/workflows/hashes.yml b/.github/workflows/hashes.yml new file mode 100644 index 000000000..2efb065c4 --- /dev/null +++ b/.github/workflows/hashes.yml @@ -0,0 +1,28 @@ +name: Generate Hashes + +on: + release: + types: [published] + +jobs: + build: + name: Generate Hashes + env: + commit_secret: ${{ secrets.commit_secret }} + runs-on: ubuntu-latest + steps: + + - name: Generate Hashes + uses: MCJack123/ghaction-generate-release-hashes@v1 + with: + hash-type: sha256 + file-name: hashes.txt + + - name: Upload Hashes to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: hashes.txt + overwrite: true + asset_name: hashes.txt + tag: ${{ github.ref }} diff --git a/bump_version.sh b/bump_version.sh new file mode 100644 index 000000000..fc7cf9389 --- /dev/null +++ b/bump_version.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +echo "Bump version..." + +current_version=$(grep "version: 5.0." pubspec.yaml | cut -f2 -d "+" ) +echo "Current Version: $current_version" + +new_vesion=$((current_version+1)) +echo "New Version: $new_vesion" + +# pubspec.foss.yaml +# pubspec.yaml +# flatpak.yml +# com.invoiceninja.app.metainfo +# contants +# snapcraft \ No newline at end of file From c21af52e737ae6a57ac8b943245525e9ee971381 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 16:12:30 +0200 Subject: [PATCH 046/138] Flatpak --- .github/workflows/hashes.yml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/hashes.yml b/.github/workflows/hashes.yml index 2efb065c4..1fd6e3436 100644 --- a/.github/workflows/hashes.yml +++ b/.github/workflows/hashes.yml @@ -18,11 +18,16 @@ jobs: hash-type: sha256 file-name: hashes.txt - - name: Upload Hashes to release - uses: svenstaro/upload-release-action@v2 + - uses: actions/upload-artifact@v2 with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - file: hashes.txt - overwrite: true - asset_name: hashes.txt - tag: ${{ github.ref }} + name: Asset Hashes + path: hashes.txt + +# - name: Upload Hashes to release +# uses: svenstaro/upload-release-action@v2 +# with: +# repo_token: ${{ secrets.GITHUB_TOKEN }} +# file: hashes.txt +# overwrite: true +# asset_name: hashes.txt +# tag: ${{ github.ref }} From cdbdb369cb8b55c46c23a690fecb4ac669bbb49d Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 16:21:13 +0200 Subject: [PATCH 047/138] Bump version script --- bump_version.sh | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/bump_version.sh b/bump_version.sh index fc7cf9389..a5fc6ea46 100644 --- a/bump_version.sh +++ b/bump_version.sh @@ -1,15 +1,17 @@ #!/bin/bash -echo "Bump version..." - current_version=$(grep "version: 5.0." pubspec.yaml | cut -f2 -d "+" ) -echo "Current Version: $current_version" - new_vesion=$((current_version+1)) -echo "New Version: $new_vesion" -# pubspec.foss.yaml +echo "Bump version... $current_version => $new_vesion" + +sed -i -e "s/version: 5.0.$current_version+$current_version/version: 5.0.$new_vesion+$new_vesion/g" ./pubspec.yaml +sed -i -e "s/version: 5.0.$current_version+$current_version/version: 5.0.$new_vesion+$new_vesion/g" ./pubspec.foss.yaml + + + # pubspec.yaml +# pubspec.foss.yaml # flatpak.yml # com.invoiceninja.app.metainfo # contants From f22e44658a7cf66a83c9ddf956630256685f05bc Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 16:25:41 +0200 Subject: [PATCH 048/138] Bump version script --- bump_version.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/bump_version.sh b/bump_version.sh index a5fc6ea46..6ff71e21a 100644 --- a/bump_version.sh +++ b/bump_version.sh @@ -5,9 +5,15 @@ new_vesion=$((current_version+1)) echo "Bump version... $current_version => $new_vesion" -sed -i -e "s/version: 5.0.$current_version+$current_version/version: 5.0.$new_vesion+$new_vesion/g" ./pubspec.yaml -sed -i -e "s/version: 5.0.$current_version+$current_version/version: 5.0.$new_vesion+$new_vesion/g" ./pubspec.foss.yaml +#sed -i -e "s/version: 5.0.$current_version+$current_version/version: 5.0.$new_vesion+$new_vesion/g" ./pubspec.yaml +#sed -i -e "s/version: 5.0.$current_version+$current_version/version: 5.0.$new_vesion+$new_vesion/g" ./pubspec.foss.yaml +sed -i -e "s/v5.0.$current_version/v5.0.$new_vesion/g" ./.github/workflows/flatpak.yml + +#sed -i -e "s///g" ./pubspec.foss.yaml +#sed -i -e "s///g" ./pubspec.foss.yaml +#sed -i -e "s///g" ./pubspec.foss.yaml +#sed -i -e "s///g" ./pubspec.foss.yaml # pubspec.yaml @@ -15,4 +21,4 @@ sed -i -e "s/version: 5.0.$current_version+$current_version/version: 5.0.$new_ve # flatpak.yml # com.invoiceninja.app.metainfo # contants -# snapcraft \ No newline at end of file +# snapcraft From d057c841d426b17da8467c6d2a78c9f85a646668 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 16:39:38 +0200 Subject: [PATCH 049/138] Flatpak --- .github/workflows/hashes.yml | 2 +- bump_version.sh | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/hashes.yml b/.github/workflows/hashes.yml index 1fd6e3436..bcb6f76c5 100644 --- a/.github/workflows/hashes.yml +++ b/.github/workflows/hashes.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Generate Hashes - uses: MCJack123/ghaction-generate-release-hashes@v1 + uses: MCJack123/ghaction-generate-release-hashes@v4 with: hash-type: sha256 file-name: hashes.txt diff --git a/bump_version.sh b/bump_version.sh index 6ff71e21a..795e77425 100644 --- a/bump_version.sh +++ b/bump_version.sh @@ -7,18 +7,19 @@ echo "Bump version... $current_version => $new_vesion" #sed -i -e "s/version: 5.0.$current_version+$current_version/version: 5.0.$new_vesion+$new_vesion/g" ./pubspec.yaml #sed -i -e "s/version: 5.0.$current_version+$current_version/version: 5.0.$new_vesion+$new_vesion/g" ./pubspec.foss.yaml +#sed -i -e "s/v5.0.$current_version/v5.0.$new_vesion/g" ./.github/workflows/flatpak.yml + +sed -i -e 's//\n /g' ./flatpak/com.invoiceninja.app.metainfo.xml -sed -i -e "s/v5.0.$current_version/v5.0.$new_vesion/g" ./.github/workflows/flatpak.yml -#sed -i -e "s///g" ./pubspec.foss.yaml #sed -i -e "s///g" ./pubspec.foss.yaml #sed -i -e "s///g" ./pubspec.foss.yaml #sed -i -e "s///g" ./pubspec.foss.yaml -# pubspec.yaml -# pubspec.foss.yaml -# flatpak.yml +# X pubspec.yaml +# X pubspec.foss.yaml +# X flatpak.yml # com.invoiceninja.app.metainfo # contants # snapcraft From f094cc75ff4c83b7e1d385c759704f643574d635 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 16:44:36 +0200 Subject: [PATCH 050/138] Bump version script --- bump_version.sh | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/bump_version.sh b/bump_version.sh index 795e77425..0ec93bfad 100644 --- a/bump_version.sh +++ b/bump_version.sh @@ -5,21 +5,9 @@ new_vesion=$((current_version+1)) echo "Bump version... $current_version => $new_vesion" -#sed -i -e "s/version: 5.0.$current_version+$current_version/version: 5.0.$new_vesion+$new_vesion/g" ./pubspec.yaml -#sed -i -e "s/version: 5.0.$current_version+$current_version/version: 5.0.$new_vesion+$new_vesion/g" ./pubspec.foss.yaml -#sed -i -e "s/v5.0.$current_version/v5.0.$new_vesion/g" ./.github/workflows/flatpak.yml - +sed -i -e "s/version: 5.0.$current_version+$current_version/version: 5.0.$new_vesion+$new_vesion/g" ./pubspec.yaml +sed -i -e "s/version: 5.0.$current_version+$current_version/version: 5.0.$new_vesion+$new_vesion/g" ./pubspec.foss.yaml +sed -i -e "s/v5.0.$current_version/v5.0.$new_vesion/g" ./.github/workflows/flatpak.yml sed -i -e 's//\n /g' ./flatpak/com.invoiceninja.app.metainfo.xml - - -#sed -i -e "s///g" ./pubspec.foss.yaml -#sed -i -e "s///g" ./pubspec.foss.yaml -#sed -i -e "s///g" ./pubspec.foss.yaml - - -# X pubspec.yaml -# X pubspec.foss.yaml -# X flatpak.yml -# com.invoiceninja.app.metainfo -# contants -# snapcraft +sed -i -e "s/kClientVersion = '5.0.$current_version'/kClientVersion = '5.0.$new_vesion'/g" ./lib/constants.dart +sed -i -e "s/version: '5.0.$current_version'/version: '5.0.$new_vesion'/g" ./snap/snapcraft.yaml \ No newline at end of file From 16c6c0454306c3222b72a4351f981d2798af704e Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 17:03:20 +0200 Subject: [PATCH 051/138] Flatpak --- .github/workflows/flatpak.yml | 10 ++++++++++ .github/workflows/hashes.yml | 33 --------------------------------- 2 files changed, 10 insertions(+), 33 deletions(-) delete mode 100644 .github/workflows/hashes.yml diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index bf79ac5aa..722e0628a 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -57,7 +57,10 @@ jobs: cd build/linux/x64/release/bundle || exit tar -czaf $archiveName ./* + shasum -a 256 $archiveName > sha256.txt + mv $archiveName "$baseDir"/ + mv sha256.txt "$baseDir"/ - name: Upload app archive to workflow uses: actions/upload-artifact@v3 @@ -65,6 +68,12 @@ jobs: name: Invoice-Ninja-Archive path: Invoice-Ninja-Linux-Portable.tar.gz + - name: Upload app archive hash to workflow + uses: actions/upload-artifact@v3 + with: + name: Invoice-Ninja-Hash + path: sha256.txt + - name: Download artifacts uses: actions/download-artifact@v3 with: @@ -80,6 +89,7 @@ jobs: automatic_release_tag: "v5.0.128" files: | ${{ github.workspace }}/artifacts/Invoice-Ninja-Archive + ${{ github.workspace }}/artifacts/Invoice-Ninja-Hash # build-flatpak: # name: Build flatpak diff --git a/.github/workflows/hashes.yml b/.github/workflows/hashes.yml deleted file mode 100644 index bcb6f76c5..000000000 --- a/.github/workflows/hashes.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Generate Hashes - -on: - release: - types: [published] - -jobs: - build: - name: Generate Hashes - env: - commit_secret: ${{ secrets.commit_secret }} - runs-on: ubuntu-latest - steps: - - - name: Generate Hashes - uses: MCJack123/ghaction-generate-release-hashes@v4 - with: - hash-type: sha256 - file-name: hashes.txt - - - uses: actions/upload-artifact@v2 - with: - name: Asset Hashes - path: hashes.txt - -# - name: Upload Hashes to release -# uses: svenstaro/upload-release-action@v2 -# with: -# repo_token: ${{ secrets.GITHUB_TOKEN }} -# file: hashes.txt -# overwrite: true -# asset_name: hashes.txt -# tag: ${{ github.ref }} From cdf2ff03fbf6d613ea4e6ac29753f627215997c8 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 17:10:25 +0200 Subject: [PATCH 052/138] Flatpak --- .github/workflows/flatpak.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 722e0628a..44731b063 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -72,7 +72,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: Invoice-Ninja-Hash - path: sha256.txt + path: Hash.txt - name: Download artifacts uses: actions/download-artifact@v3 From 7105b748e09158314f36fe302aa75ebe08ebe6ed Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 17:19:55 +0200 Subject: [PATCH 053/138] Flatpak --- .github/workflows/flatpak.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 44731b063..938d605bd 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -57,10 +57,10 @@ jobs: cd build/linux/x64/release/bundle || exit tar -czaf $archiveName ./* - shasum -a 256 $archiveName > sha256.txt + shasum -a 256 $archiveName > Hash.txt mv $archiveName "$baseDir"/ - mv sha256.txt "$baseDir"/ + mv Hash.txt "$baseDir"/ - name: Upload app archive to workflow uses: actions/upload-artifact@v3 From d2a4202375b79701b403c3fb537635f30657fb8d Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 17:30:49 +0200 Subject: [PATCH 054/138] Flatpak --- .github/workflows/flatpak.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 938d605bd..75a04bb2b 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -57,10 +57,10 @@ jobs: cd build/linux/x64/release/bundle || exit tar -czaf $archiveName ./* - shasum -a 256 $archiveName > Hash.txt + shasum -a 256 $archiveName > Hashes.txt mv $archiveName "$baseDir"/ - mv Hash.txt "$baseDir"/ + mv Hashes.txt "$baseDir"/ - name: Upload app archive to workflow uses: actions/upload-artifact@v3 @@ -72,7 +72,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: Invoice-Ninja-Hash - path: Hash.txt + path: Hashes.txt - name: Download artifacts uses: actions/download-artifact@v3 From fb5edc96e4c1bc5c70d20d5c533d4c2fe80111e6 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 18:04:16 +0200 Subject: [PATCH 055/138] Flatpak --- .github/workflows/flatpak.yml | 2 +- ...invoiceninja.app.svg => com.invoiceninja.InvoiceNinja.svg} | 0 bump_version.sh | 2 +- ...inja.app.desktop => com.invoiceninja.InvoiceNinja.desktop} | 2 +- ...etainfo.xml => com.invoiceninja.InvoiceNinja.metainfo.xml} | 4 ++-- 5 files changed, 5 insertions(+), 5 deletions(-) rename assets/images/{com.invoiceninja.app.svg => com.invoiceninja.InvoiceNinja.svg} (100%) rename flatpak/{com.invoiceninja.app.desktop => com.invoiceninja.InvoiceNinja.desktop} (87%) rename flatpak/{com.invoiceninja.app.metainfo.xml => com.invoiceninja.InvoiceNinja.metainfo.xml} (95%) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 75a04bb2b..dd3331663 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -11,7 +11,7 @@ on: # cancel-in-progress: true env: - project-id: com.invoiceninja.app + project-id: com.invoiceninja.InvoiceNinja jobs: build-flutter-app: diff --git a/assets/images/com.invoiceninja.app.svg b/assets/images/com.invoiceninja.InvoiceNinja.svg similarity index 100% rename from assets/images/com.invoiceninja.app.svg rename to assets/images/com.invoiceninja.InvoiceNinja.svg diff --git a/bump_version.sh b/bump_version.sh index 0ec93bfad..c1cd5b324 100644 --- a/bump_version.sh +++ b/bump_version.sh @@ -8,6 +8,6 @@ echo "Bump version... $current_version => $new_vesion" sed -i -e "s/version: 5.0.$current_version+$current_version/version: 5.0.$new_vesion+$new_vesion/g" ./pubspec.yaml sed -i -e "s/version: 5.0.$current_version+$current_version/version: 5.0.$new_vesion+$new_vesion/g" ./pubspec.foss.yaml sed -i -e "s/v5.0.$current_version/v5.0.$new_vesion/g" ./.github/workflows/flatpak.yml -sed -i -e 's//\n /g' ./flatpak/com.invoiceninja.app.metainfo.xml +sed -i -e 's//\n /g' ./flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml sed -i -e "s/kClientVersion = '5.0.$current_version'/kClientVersion = '5.0.$new_vesion'/g" ./lib/constants.dart sed -i -e "s/version: '5.0.$current_version'/version: '5.0.$new_vesion'/g" ./snap/snapcraft.yaml \ No newline at end of file diff --git a/flatpak/com.invoiceninja.app.desktop b/flatpak/com.invoiceninja.InvoiceNinja.desktop similarity index 87% rename from flatpak/com.invoiceninja.app.desktop rename to flatpak/com.invoiceninja.InvoiceNinja.desktop index 2b0704d5a..a0928a4d5 100644 --- a/flatpak/com.invoiceninja.app.desktop +++ b/flatpak/com.invoiceninja.InvoiceNinja.desktop @@ -6,7 +6,7 @@ Name=Invoice Ninja Comment=Create invoices, accept payments, track expenses & time tasks Categories=Productivity; -Icon=com.invoiceninja.app +Icon=com.invoiceninja.InvoiceNinja Exec=invoiceninja_client Terminal=false StartupWMClass=invoiceninja_client \ No newline at end of file diff --git a/flatpak/com.invoiceninja.app.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml similarity index 95% rename from flatpak/com.invoiceninja.app.metainfo.xml rename to flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index de22643c3..0396779e2 100644 --- a/flatpak/com.invoiceninja.app.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -4,7 +4,7 @@ https://www.freedesktop.org/software/appstream/metainfocreator/#/ --> - com.invoiceninja.app + com.invoiceninja.InvoiceNinja Invoice Ninja

Create invoices, accept payments, track expenses & time tasks Hillel Coren @@ -39,7 +39,7 @@ All of these features combine to help you receive the money you deserve and reduce the amount of time you spend on repetitive invoicing tasks. Spend less time on paperwork and more time at your craft.

- com.invoiceninja.app.desktop + com.invoiceninja.InvoiceNinja.desktop https://github.com/invoiceninja/admin-portal/blob/master/samples/screenshots/5.png From 15a5157516745c9d0a9ed9490f66c4e4bd17f494 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 18:07:22 +0200 Subject: [PATCH 056/138] Update version --- .github/workflows/flatpak.yml | 2 +- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 2 +- lib/constants.dart | 2 +- pubspec.foss.yaml | 2 +- pubspec.yaml | 2 +- snap/snapcraft.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index dd3331663..6096c1886 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -86,7 +86,7 @@ jobs: draft: false prerelease: false title: "Latest Release" - automatic_release_tag: "v5.0.128" + automatic_release_tag: "v5.0.129" files: | ${{ github.workspace }}/artifacts/Invoice-Ninja-Archive ${{ github.workspace }}/artifacts/Invoice-Ninja-Hash diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index 0396779e2..b7041aba6 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -47,6 +47,6 @@ - + \ No newline at end of file diff --git a/lib/constants.dart b/lib/constants.dart index 011fbf055..f16613afc 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -4,7 +4,7 @@ class Constants { } // TODO remove version once #46609 is fixed -const String kClientVersion = '5.0.128'; +const String kClientVersion = '5.0.129'; const String kMinServerVersion = '5.0.4'; const String kAppName = 'Invoice Ninja'; diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index 450d2b66c..9adee940b 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.128+128 +version: 5.0.129+129 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/pubspec.yaml b/pubspec.yaml index 2805ca5ca..7b4fd0669 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.128+128 +version: 5.0.129+129 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 20900ff0a..195322e22 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: invoiceninja -version: '5.0.128' +version: '5.0.129' summary: Create invoices, accept payments, track expenses & time tasks description: "### Note: if the app fails to run using `snap run invoiceninja` it may help to run `/snap/invoiceninja/current/bin/invoiceninja` instead From a28b2d5560aab335cd8efa2f62d38d866c0d01e8 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 19:00:02 +0200 Subject: [PATCH 057/138] Flatpak --- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index b7041aba6..8691d8ea1 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -18,7 +18,7 @@

Create. Send. Get Paid.

-

Invoice Ninja is a leading source-code available platform for SMB’s to invoice, accept payments, track expenses & time billable-tasks. Designed for freelancers and small to medium size businesses, Invoice Ninja is a suite of apps to help you get paid.

+

Invoice Ninja is a leading source-code available platform for SMB’s to invoice, accept payments, track expenses & time billable-tasks. Designed for freelancers and small to medium size businesses, Invoice Ninja is a suite of apps to help you get paid.

• Incredibly easy to use
Invoice Ninja was built to serve freelancers and business owners with a complete suite of invoicing & payment tools to advance your business. From 855d927bee151e41fdbbb279dadee3c8d96bf5be Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 19:00:36 +0200 Subject: [PATCH 058/138] Update version --- .github/workflows/flatpak.yml | 2 +- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 2 +- lib/constants.dart | 2 +- pubspec.foss.yaml | 2 +- pubspec.yaml | 2 +- snap/snapcraft.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 6096c1886..45b31e12b 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -86,7 +86,7 @@ jobs: draft: false prerelease: false title: "Latest Release" - automatic_release_tag: "v5.0.129" + automatic_release_tag: "v5.0.130" files: | ${{ github.workspace }}/artifacts/Invoice-Ninja-Archive ${{ github.workspace }}/artifacts/Invoice-Ninja-Hash diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index 8691d8ea1..632a590b4 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -47,6 +47,6 @@ - + \ No newline at end of file diff --git a/lib/constants.dart b/lib/constants.dart index f16613afc..82a9dfcc7 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -4,7 +4,7 @@ class Constants { } // TODO remove version once #46609 is fixed -const String kClientVersion = '5.0.129'; +const String kClientVersion = '5.0.130'; const String kMinServerVersion = '5.0.4'; const String kAppName = 'Invoice Ninja'; diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index 9adee940b..35866450e 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.129+129 +version: 5.0.130+130 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/pubspec.yaml b/pubspec.yaml index 7b4fd0669..041750ccb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.129+129 +version: 5.0.130+130 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 195322e22..e5121b559 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: invoiceninja -version: '5.0.129' +version: '5.0.130' summary: Create invoices, accept payments, track expenses & time tasks description: "### Note: if the app fails to run using `snap run invoiceninja` it may help to run `/snap/invoiceninja/current/bin/invoiceninja` instead From e1580ce7dccd771ba24c5362713a3385491cfeb3 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 19:13:35 +0200 Subject: [PATCH 059/138] Flatpak --- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index 632a590b4..a100c3992 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -42,7 +42,7 @@ com.invoiceninja.InvoiceNinja.desktop - https://github.com/invoiceninja/admin-portal/blob/master/samples/screenshots/5.png + https://raw.githubusercontent.com/invoiceninja/admin-portal/master/samples/screenshots/5.png From b7553fb78b566067c181dfc105909c72aba8e989 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 19:14:05 +0200 Subject: [PATCH 060/138] Update version --- .github/workflows/flatpak.yml | 2 +- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 2 +- lib/constants.dart | 2 +- pubspec.foss.yaml | 2 +- pubspec.yaml | 2 +- snap/snapcraft.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 45b31e12b..e1616bdd0 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -86,7 +86,7 @@ jobs: draft: false prerelease: false title: "Latest Release" - automatic_release_tag: "v5.0.130" + automatic_release_tag: "v5.0.131" files: | ${{ github.workspace }}/artifacts/Invoice-Ninja-Archive ${{ github.workspace }}/artifacts/Invoice-Ninja-Hash diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index a100c3992..2259614d0 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -47,6 +47,6 @@ - + \ No newline at end of file diff --git a/lib/constants.dart b/lib/constants.dart index 82a9dfcc7..f921fa79f 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -4,7 +4,7 @@ class Constants { } // TODO remove version once #46609 is fixed -const String kClientVersion = '5.0.130'; +const String kClientVersion = '5.0.131'; const String kMinServerVersion = '5.0.4'; const String kAppName = 'Invoice Ninja'; diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index 35866450e..01ee835d9 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.130+130 +version: 5.0.131+131 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/pubspec.yaml b/pubspec.yaml index 041750ccb..6b370129f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.130+130 +version: 5.0.131+131 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index e5121b559..d0b862e40 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: invoiceninja -version: '5.0.130' +version: '5.0.131' summary: Create invoices, accept payments, track expenses & time tasks description: "### Note: if the app fails to run using `snap run invoiceninja` it may help to run `/snap/invoiceninja/current/bin/invoiceninja` instead From baec40c17325e81005c8e776d0d4e679d9eb2ed3 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 20:11:55 +0200 Subject: [PATCH 061/138] Flatpak --- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index 2259614d0..d06109c15 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -9,8 +9,8 @@

Create invoices, accept payments, track expenses & time tasks Hillel Coren https://invoiceninja.com - AAL - AAL + BSD + BSD pointing keyboard From 5a092f2fa2530f8c19e0a836e659c0ebc3eafdf3 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 20:12:14 +0200 Subject: [PATCH 062/138] Update version --- .github/workflows/flatpak.yml | 2 +- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 2 +- lib/constants.dart | 2 +- pubspec.foss.yaml | 2 +- pubspec.yaml | 2 +- snap/snapcraft.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index e1616bdd0..3de53230a 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -86,7 +86,7 @@ jobs: draft: false prerelease: false title: "Latest Release" - automatic_release_tag: "v5.0.131" + automatic_release_tag: "v5.0.132" files: | ${{ github.workspace }}/artifacts/Invoice-Ninja-Archive ${{ github.workspace }}/artifacts/Invoice-Ninja-Hash diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index d06109c15..9b336100d 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -47,6 +47,6 @@ - + \ No newline at end of file diff --git a/lib/constants.dart b/lib/constants.dart index f921fa79f..1345fd172 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -4,7 +4,7 @@ class Constants { } // TODO remove version once #46609 is fixed -const String kClientVersion = '5.0.131'; +const String kClientVersion = '5.0.132'; const String kMinServerVersion = '5.0.4'; const String kAppName = 'Invoice Ninja'; diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index 01ee835d9..90e36da0b 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.131+131 +version: 5.0.132+132 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/pubspec.yaml b/pubspec.yaml index 6b370129f..0c37df354 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.131+131 +version: 5.0.132+132 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index d0b862e40..5632c42e1 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: invoiceninja -version: '5.0.131' +version: '5.0.132' summary: Create invoices, accept payments, track expenses & time tasks description: "### Note: if the app fails to run using `snap run invoiceninja` it may help to run `/snap/invoiceninja/current/bin/invoiceninja` instead From dda7543d253aed483ab4fa59361d4b23ea26caa2 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 20:37:56 +0200 Subject: [PATCH 063/138] Flatpak --- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index 9b336100d..dc37bc4b1 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -9,8 +9,8 @@ Create invoices, accept payments, track expenses & time tasks Hillel Coren https://invoiceninja.com - BSD - BSD + BSD-3-Clause + BSD-3-Clause pointing keyboard From 51a68c7800fe8e9cda6a95f899f0d317ead732ab Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 20:38:12 +0200 Subject: [PATCH 064/138] Update version --- .github/workflows/flatpak.yml | 2 +- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 2 +- lib/constants.dart | 2 +- pubspec.foss.yaml | 2 +- pubspec.yaml | 2 +- snap/snapcraft.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 3de53230a..dc94253ad 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -86,7 +86,7 @@ jobs: draft: false prerelease: false title: "Latest Release" - automatic_release_tag: "v5.0.132" + automatic_release_tag: "v5.0.133" files: | ${{ github.workspace }}/artifacts/Invoice-Ninja-Archive ${{ github.workspace }}/artifacts/Invoice-Ninja-Hash diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index dc37bc4b1..b2530ff5a 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -47,6 +47,6 @@ - + \ No newline at end of file diff --git a/lib/constants.dart b/lib/constants.dart index 1345fd172..c644314a6 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -4,7 +4,7 @@ class Constants { } // TODO remove version once #46609 is fixed -const String kClientVersion = '5.0.132'; +const String kClientVersion = '5.0.133'; const String kMinServerVersion = '5.0.4'; const String kAppName = 'Invoice Ninja'; diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index 90e36da0b..bd056c118 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.132+132 +version: 5.0.133+133 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/pubspec.yaml b/pubspec.yaml index 0c37df354..c8b662587 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.132+132 +version: 5.0.133+133 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 5632c42e1..ecb539328 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: invoiceninja -version: '5.0.132' +version: '5.0.133' summary: Create invoices, accept payments, track expenses & time tasks description: "### Note: if the app fails to run using `snap run invoiceninja` it may help to run `/snap/invoiceninja/current/bin/invoiceninja` instead From 0b60bd64a69045e6f1109131742a4eb843788274 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 21:13:48 +0200 Subject: [PATCH 065/138] Flatpak --- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index b2530ff5a..74374861f 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -9,7 +9,6 @@ Create invoices, accept payments, track expenses & time tasks Hillel Coren https://invoiceninja.com - BSD-3-Clause BSD-3-Clause pointing From 20c3a00744e52c3a76e05295669fb114fdb15c5a Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 21:14:14 +0200 Subject: [PATCH 066/138] Update version --- .github/workflows/flatpak.yml | 2 +- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 2 +- lib/constants.dart | 2 +- pubspec.foss.yaml | 2 +- pubspec.yaml | 2 +- snap/snapcraft.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index dc94253ad..3b80bb7aa 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -86,7 +86,7 @@ jobs: draft: false prerelease: false title: "Latest Release" - automatic_release_tag: "v5.0.133" + automatic_release_tag: "v5.0.134" files: | ${{ github.workspace }}/artifacts/Invoice-Ninja-Archive ${{ github.workspace }}/artifacts/Invoice-Ninja-Hash diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index 74374861f..8c0371310 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -46,6 +46,6 @@ - + \ No newline at end of file diff --git a/lib/constants.dart b/lib/constants.dart index c644314a6..c2ffb9843 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -4,7 +4,7 @@ class Constants { } // TODO remove version once #46609 is fixed -const String kClientVersion = '5.0.133'; +const String kClientVersion = '5.0.134'; const String kMinServerVersion = '5.0.4'; const String kAppName = 'Invoice Ninja'; diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index bd056c118..63aebf16c 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.133+133 +version: 5.0.134+134 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/pubspec.yaml b/pubspec.yaml index c8b662587..7a31969e5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.133+133 +version: 5.0.134+134 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index ecb539328..e2aec084f 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: invoiceninja -version: '5.0.133' +version: '5.0.134' summary: Create invoices, accept payments, track expenses & time tasks description: "### Note: if the app fails to run using `snap run invoiceninja` it may help to run `/snap/invoiceninja/current/bin/invoiceninja` instead From 9387d99c76a66287aa4137637f2ace32450a02cc Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 21:21:36 +0200 Subject: [PATCH 067/138] Flatpak --- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index 8c0371310..9ee9c6e53 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -9,7 +9,7 @@ Create invoices, accept payments, track expenses & time tasks Hillel Coren https://invoiceninja.com - BSD-3-Clause + AAL pointing keyboard From 5bb93a5e7832d251eebe5ad185a2e31987351648 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 21:21:59 +0200 Subject: [PATCH 068/138] Update version --- .github/workflows/flatpak.yml | 2 +- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 2 +- lib/constants.dart | 2 +- pubspec.foss.yaml | 2 +- pubspec.yaml | 2 +- snap/snapcraft.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 3b80bb7aa..f899a0e85 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -86,7 +86,7 @@ jobs: draft: false prerelease: false title: "Latest Release" - automatic_release_tag: "v5.0.134" + automatic_release_tag: "v5.0.135" files: | ${{ github.workspace }}/artifacts/Invoice-Ninja-Archive ${{ github.workspace }}/artifacts/Invoice-Ninja-Hash diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index 9ee9c6e53..ff5e54b0d 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -46,6 +46,6 @@ - + \ No newline at end of file diff --git a/lib/constants.dart b/lib/constants.dart index c2ffb9843..4a062674a 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -4,7 +4,7 @@ class Constants { } // TODO remove version once #46609 is fixed -const String kClientVersion = '5.0.134'; +const String kClientVersion = '5.0.135'; const String kMinServerVersion = '5.0.4'; const String kAppName = 'Invoice Ninja'; diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index 63aebf16c..a8affec81 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.134+134 +version: 5.0.135+135 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/pubspec.yaml b/pubspec.yaml index 7a31969e5..0b83e2caf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.134+134 +version: 5.0.135+135 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index e2aec084f..a381de031 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: invoiceninja -version: '5.0.134' +version: '5.0.135' summary: Create invoices, accept payments, track expenses & time tasks description: "### Note: if the app fails to run using `snap run invoiceninja` it may help to run `/snap/invoiceninja/current/bin/invoiceninja` instead From e8f78e703c87f43da6e222df534692598c387855 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 21:28:27 +0200 Subject: [PATCH 069/138] Flatpak --- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index ff5e54b0d..d53f29f60 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -10,6 +10,7 @@ Hillel Coren https://invoiceninja.com AAL + CC0-1.0 pointing keyboard From 4dcfe80fe363f7cf65779375d9dd3dbacf0900fe Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 21:28:53 +0200 Subject: [PATCH 070/138] Update version --- .github/workflows/flatpak.yml | 2 +- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 2 +- lib/constants.dart | 2 +- pubspec.foss.yaml | 2 +- pubspec.yaml | 2 +- snap/snapcraft.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index f899a0e85..a57ff6d08 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -86,7 +86,7 @@ jobs: draft: false prerelease: false title: "Latest Release" - automatic_release_tag: "v5.0.135" + automatic_release_tag: "v5.0.136" files: | ${{ github.workspace }}/artifacts/Invoice-Ninja-Archive ${{ github.workspace }}/artifacts/Invoice-Ninja-Hash diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index d53f29f60..d6dc4a7cf 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -47,6 +47,6 @@ - + \ No newline at end of file diff --git a/lib/constants.dart b/lib/constants.dart index 4a062674a..732d7d376 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -4,7 +4,7 @@ class Constants { } // TODO remove version once #46609 is fixed -const String kClientVersion = '5.0.135'; +const String kClientVersion = '5.0.136'; const String kMinServerVersion = '5.0.4'; const String kAppName = 'Invoice Ninja'; diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index a8affec81..05e808fe0 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.135+135 +version: 5.0.136+136 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/pubspec.yaml b/pubspec.yaml index 0b83e2caf..18b221eb8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.135+135 +version: 5.0.136+136 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index a381de031..9ceb0a3dc 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: invoiceninja -version: '5.0.135' +version: '5.0.136' summary: Create invoices, accept payments, track expenses & time tasks description: "### Note: if the app fails to run using `snap run invoiceninja` it may help to run `/snap/invoiceninja/current/bin/invoiceninja` instead From 0ff65a4a8141edc0bbef9cd6a33126eab910658a Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Nov 2023 21:39:28 +0200 Subject: [PATCH 071/138] Bump version --- bump_version.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bump_version.sh b/bump_version.sh index c1cd5b324..d2e49108a 100644 --- a/bump_version.sh +++ b/bump_version.sh @@ -2,12 +2,13 @@ current_version=$(grep "version: 5.0." pubspec.yaml | cut -f2 -d "+" ) new_vesion=$((current_version+1)) +date_today=$(date +%F) echo "Bump version... $current_version => $new_vesion" sed -i -e "s/version: 5.0.$current_version+$current_version/version: 5.0.$new_vesion+$new_vesion/g" ./pubspec.yaml sed -i -e "s/version: 5.0.$current_version+$current_version/version: 5.0.$new_vesion+$new_vesion/g" ./pubspec.foss.yaml sed -i -e "s/v5.0.$current_version/v5.0.$new_vesion/g" ./.github/workflows/flatpak.yml -sed -i -e 's//\n /g' ./flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +sed -i -e 's//\n /g' ./flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml sed -i -e "s/kClientVersion = '5.0.$current_version'/kClientVersion = '5.0.$new_vesion'/g" ./lib/constants.dart sed -i -e "s/version: '5.0.$current_version'/version: '5.0.$new_vesion'/g" ./snap/snapcraft.yaml \ No newline at end of file From 320e2eff1f91c23e59cb1d5333a22158be9f500b Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 2 Nov 2023 17:56:21 +0200 Subject: [PATCH 072/138] Update Flatpak metadata --- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index d6dc4a7cf..6ad26ea33 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -7,7 +7,7 @@ com.invoiceninja.InvoiceNinja Invoice Ninja Create invoices, accept payments, track expenses & time tasks - Hillel Coren + Invoice Ninja https://invoiceninja.com AAL CC0-1.0 From 8d4edf2709bbefb496811db4bcdf3496e48fdd88 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 2 Nov 2023 18:04:58 +0200 Subject: [PATCH 073/138] Update version --- .github/workflows/flatpak.yml | 2 +- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 1 + lib/constants.dart | 2 +- pubspec.foss.yaml | 2 +- pubspec.yaml | 2 +- snap/snapcraft.yaml | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index a57ff6d08..21e56c937 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -86,7 +86,7 @@ jobs: draft: false prerelease: false title: "Latest Release" - automatic_release_tag: "v5.0.136" + automatic_release_tag: "v5.0.137" files: | ${{ github.workspace }}/artifacts/Invoice-Ninja-Archive ${{ github.workspace }}/artifacts/Invoice-Ninja-Hash diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index 6ad26ea33..eab6fcb51 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -47,6 +47,7 @@ + \ No newline at end of file diff --git a/lib/constants.dart b/lib/constants.dart index 732d7d376..3ed5ce9c8 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -4,7 +4,7 @@ class Constants { } // TODO remove version once #46609 is fixed -const String kClientVersion = '5.0.136'; +const String kClientVersion = '5.0.137'; const String kMinServerVersion = '5.0.4'; const String kAppName = 'Invoice Ninja'; diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index 05e808fe0..269a1c85b 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.136+136 +version: 5.0.137+137 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/pubspec.yaml b/pubspec.yaml index 18b221eb8..1b7b03952 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.136+136 +version: 5.0.137+137 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 9ceb0a3dc..a71a4f4b0 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: invoiceninja -version: '5.0.136' +version: '5.0.137' summary: Create invoices, accept payments, track expenses & time tasks description: "### Note: if the app fails to run using `snap run invoiceninja` it may help to run `/snap/invoiceninja/current/bin/invoiceninja` instead From a2b45c4a1ad9fb2d7b9e663dfbed6f73b06233fd Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 3 Nov 2023 11:34:29 +0200 Subject: [PATCH 074/138] Update Flatpak icon --- .../images/com.invoiceninja.InvoiceNinja.svg | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/assets/images/com.invoiceninja.InvoiceNinja.svg b/assets/images/com.invoiceninja.InvoiceNinja.svg index 8e838c8f8..f0b3482ed 100644 --- a/assets/images/com.invoiceninja.InvoiceNinja.svg +++ b/assets/images/com.invoiceninja.InvoiceNinja.svg @@ -1,2 +1,31 @@ - -Invoice Ninja icon \ No newline at end of file + + + + + + + + From 49742da8c19f2f2bdfc0cc89902ff9ed057cc2ed Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 3 Nov 2023 11:35:22 +0200 Subject: [PATCH 075/138] Update Flatpak icon --- .../images/com.invoiceninja.InvoiceNinja.svg | 187 +++++++++++++++--- 1 file changed, 156 insertions(+), 31 deletions(-) diff --git a/assets/images/com.invoiceninja.InvoiceNinja.svg b/assets/images/com.invoiceninja.InvoiceNinja.svg index f0b3482ed..8b8b1da8b 100644 --- a/assets/images/com.invoiceninja.InvoiceNinja.svg +++ b/assets/images/com.invoiceninja.InvoiceNinja.svg @@ -1,31 +1,156 @@ - - - - - - - - + + + + + + + + + + + \ No newline at end of file From 712dfa645a86a3ea7936e0550601768e44533110 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 3 Nov 2023 12:21:58 +0200 Subject: [PATCH 076/138] Apply check response to raw responses --- lib/data/web_client.dart | 8 ++++---- lib/ui/client/client_pdf.dart | 13 ------------- lib/ui/invoice/invoice_pdf.dart | 13 ------------- lib/utils/designs.dart | 8 +------- 4 files changed, 5 insertions(+), 37 deletions(-) diff --git a/lib/data/web_client.dart b/lib/data/web_client.dart index 67f728483..56a6f2e8b 100644 --- a/lib/data/web_client.dart +++ b/lib/data/web_client.dart @@ -51,12 +51,12 @@ class WebClient { ); client.close(); + _checkResponse(url, response); + if (rawResponse) { return response; } - _checkResponse(url, response); - final dynamic jsonResponse = json.decode(response.body); //debugPrint(response.body, wrapWidth: 1000); @@ -113,12 +113,12 @@ class WebClient { client.close(); } + _checkResponse(url, response); + if (rawResponse) { return response; } - _checkResponse(url, response); - return json.decode(response.body); } diff --git a/lib/ui/client/client_pdf.dart b/lib/ui/client/client_pdf.dart index 01971eda9..6f1e5c365 100644 --- a/lib/ui/client/client_pdf.dart +++ b/lib/ui/client/client_pdf.dart @@ -171,19 +171,6 @@ class _ClientPdfViewState extends State { rawResponse: true, ); - if (response!.statusCode >= 400) { - String errorMessage = - '${response.statusCode}: ${response.reasonPhrase}\n\n'; - - try { - errorMessage += jsonDecode(response.body)['message']; - } catch (error) { - errorMessage += response.body; - } - - throw errorMessage; - } - return response; } diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index 19b30188f..ff0b15d3f 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -351,18 +351,5 @@ Future _loadPDF( response = await WebClient().get(url, '', rawResponse: true); } - if (response!.statusCode >= 400) { - String errorMessage = - '${response.statusCode}: ${response.reasonPhrase}\n\n'; - - try { - errorMessage += jsonDecode(response.body)['message']; - } catch (error) { - errorMessage += response.body; - } - - throw errorMessage; - } - return response; } diff --git a/lib/utils/designs.dart b/lib/utils/designs.dart index d1b81d6c0..e3a51c9b9 100644 --- a/lib/utils/designs.dart +++ b/lib/utils/designs.dart @@ -48,13 +48,7 @@ void loadDesign({ webClient .post(url, credentials.token, data: json.encode(data), rawResponse: true) .then((dynamic response) { - if ((response as Response).statusCode >= 400) { - showErrorDialog( - message: '${response.statusCode}: ${response.reasonPhrase}'); - onComplete(null); - } else { - onComplete(response); - } + onComplete(response); }).catchError((dynamic error) { showErrorDialog(message: '$error'); onComplete(null); From 5f5735f7176020b3581560e38c6725a77b0df36b Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 3 Nov 2023 12:28:33 +0200 Subject: [PATCH 077/138] Update version --- .github/workflows/flatpak.yml | 2 +- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 1 + lib/constants.dart | 2 +- pubspec.foss.yaml | 2 +- pubspec.yaml | 2 +- snap/snapcraft.yaml | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 21e56c937..a37bbf50a 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -86,7 +86,7 @@ jobs: draft: false prerelease: false title: "Latest Release" - automatic_release_tag: "v5.0.137" + automatic_release_tag: "v5.0.138" files: | ${{ github.workspace }}/artifacts/Invoice-Ninja-Archive ${{ github.workspace }}/artifacts/Invoice-Ninja-Hash diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index eab6fcb51..c662fe055 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -47,6 +47,7 @@ + diff --git a/lib/constants.dart b/lib/constants.dart index 3ed5ce9c8..f9c90a28a 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -4,7 +4,7 @@ class Constants { } // TODO remove version once #46609 is fixed -const String kClientVersion = '5.0.137'; +const String kClientVersion = '5.0.138'; const String kMinServerVersion = '5.0.4'; const String kAppName = 'Invoice Ninja'; diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index 269a1c85b..d325990aa 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.137+137 +version: 5.0.138+138 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/pubspec.yaml b/pubspec.yaml index 1b7b03952..f5ccc9cfd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.137+137 +version: 5.0.138+138 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index a71a4f4b0..20b771b1b 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: invoiceninja -version: '5.0.137' +version: '5.0.138' summary: Create invoices, accept payments, track expenses & time tasks description: "### Note: if the app fails to run using `snap run invoiceninja` it may help to run `/snap/invoiceninja/current/bin/invoiceninja` instead From 770630615e1ec44281f6a0040bf123b9ecfe4084 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 3 Nov 2023 12:50:36 +0200 Subject: [PATCH 078/138] Update logo --- .../images/com.invoiceninja.InvoiceNinja.svg | 211 +++++------------- 1 file changed, 56 insertions(+), 155 deletions(-) diff --git a/assets/images/com.invoiceninja.InvoiceNinja.svg b/assets/images/com.invoiceninja.InvoiceNinja.svg index 8b8b1da8b..80fdeb5d3 100644 --- a/assets/images/com.invoiceninja.InvoiceNinja.svg +++ b/assets/images/com.invoiceninja.InvoiceNinja.svg @@ -1,156 +1,57 @@ + + + - - - - - - - - - - \ No newline at end of file + viewBox="0 0 792 612" enable-background="new 0 0 792 612" xml:space="preserve"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 6d1d731e3d2285696af02ad829fbba7e255883bf Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 3 Nov 2023 12:58:01 +0200 Subject: [PATCH 079/138] Update logo --- .../images/com.invoiceninja.InvoiceNinja.svg | 59 +------------------ 1 file changed, 2 insertions(+), 57 deletions(-) diff --git a/assets/images/com.invoiceninja.InvoiceNinja.svg b/assets/images/com.invoiceninja.InvoiceNinja.svg index 80fdeb5d3..8e838c8f8 100644 --- a/assets/images/com.invoiceninja.InvoiceNinja.svg +++ b/assets/images/com.invoiceninja.InvoiceNinja.svg @@ -1,57 +1,2 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +Invoice Ninja icon \ No newline at end of file From 52b3ab7e162d511c89c95f07e8923a5c9f924924 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 3 Nov 2023 16:12:38 +0200 Subject: [PATCH 080/138] Update app metadata --- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 5 ++++- snap/snapcraft.yaml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index c662fe055..47c4eb37c 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -9,6 +9,9 @@ Create invoices, accept payments, track expenses & time tasks Invoice Ninja https://invoiceninja.com + https://forum.invoiceninja.com + https://github.com/invoiceninja/admin-portal + https://github.com/invoiceninja/admin-portal/issues AAL CC0-1.0 @@ -18,7 +21,7 @@

Create. Send. Get Paid.

-

Invoice Ninja is a leading source-code available platform for SMB’s to invoice, accept payments, track expenses & time billable-tasks. Designed for freelancers and small to medium size businesses, Invoice Ninja is a suite of apps to help you get paid.

+

Invoice Ninja is a leading platform for SMB’s to invoice, accept payments, track expenses & time billable-tasks. Designed for freelancers and small to medium size businesses, Invoice Ninja is a suite of apps to help you get paid.

• Incredibly easy to use
Invoice Ninja was built to serve freelancers and business owners with a complete suite of invoicing & payment tools to advance your business. diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 20b771b1b..49de8166c 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -5,7 +5,7 @@ description: "### Note: if the app fails to run using `snap run invoiceninja` it Create. Send. Get Paid. -Invoice Ninja is a leading source-code available platform for SMB’s to invoice, accept payments, track expenses & time billable-tasks. Designed for freelancers and small to medium size businesses, Invoice Ninja is a suite of apps to help you get paid. +Invoice Ninja is a leading platform for SMB’s to invoice, accept payments, track expenses & time billable-tasks. Designed for freelancers and small to medium size businesses, Invoice Ninja is a suite of apps to help you get paid. • Incredibly easy to use Invoice Ninja was built to serve freelancers and business owners with a complete suite of invoicing & payment tools to advance your business. From c7497eabae8347636cddf7ec3184983ee587229a Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 6 Nov 2023 16:38:01 +0200 Subject: [PATCH 081/138] Hide documents state filter on mobile --- lib/ui/app/app_bottom_bar.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/ui/app/app_bottom_bar.dart b/lib/ui/app/app_bottom_bar.dart index 2af3609dc..a6214d535 100644 --- a/lib/ui/app/app_bottom_bar.dart +++ b/lib/ui/app/app_bottom_bar.dart @@ -434,9 +434,7 @@ class _AppBottomBarState extends State { onPressed: () => widget.onCheckboxPressed(), ), ], - if (isMobile(context) || - widget.entityType == EntityType.companyGateway && - widget.onSelectedState != null) + if (isMobile(context) && widget.onSelectedState != null) IconButton( tooltip: localization!.filter, icon: Icon(Icons.filter_list), From 32abfe08c710e8cb36cde141b904f5bb46d1f589 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 6 Nov 2023 16:39:41 +0200 Subject: [PATCH 082/138] Remove document FAB --- lib/ui/document/document_screen.dart | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/lib/ui/document/document_screen.dart b/lib/ui/document/document_screen.dart index dafdc443a..7c93aba31 100644 --- a/lib/ui/document/document_screen.dart +++ b/lib/ui/document/document_screen.dart @@ -8,7 +8,6 @@ import 'package:invoiceninja_flutter/constants.dart'; // Project imports: import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/data/models/static/document_status_model.dart'; -import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/document/document_actions.dart'; import 'package:invoiceninja_flutter/ui/app/app_bottom_bar.dart'; @@ -119,22 +118,6 @@ class DocumentScreen extends StatelessWidget { } }, ), - floatingActionButton: state.prefState.isMenuFloated && - userCompany.canCreate(EntityType.document) - ? FloatingActionButton( - heroTag: 'document_fab', - backgroundColor: Theme.of(context).primaryColorDark, - onPressed: () { - createEntityByType( - context: context, entityType: EntityType.document); - }, - child: Icon( - Icons.add, - color: Colors.white, - ), - tooltip: localization!.newDocument, - ) - : null, ); } } From 520cd09826e03ebc636a9dc2ffec153faffd7bad Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 6 Nov 2023 17:02:53 +0200 Subject: [PATCH 083/138] Design template changes --- lib/data/models/settings_model.dart | 12 +++ lib/data/models/settings_model.g.dart | 99 +++++++++++++++++++ lib/ui/app/forms/design_picker.dart | 5 +- lib/ui/design/edit/design_edit.dart | 2 +- lib/ui/invoice/edit/invoice_edit_desktop.dart | 2 +- lib/ui/invoice/edit/invoice_edit_details.dart | 2 +- lib/ui/settings/company_details.dart | 8 +- lib/ui/settings/invoice_design.dart | 24 ++++- lib/utils/i18n.dart | 19 ++++ 9 files changed, 161 insertions(+), 12 deletions(-) diff --git a/lib/data/models/settings_model.dart b/lib/data/models/settings_model.dart index de9a10eab..fc028206c 100644 --- a/lib/data/models/settings_model.dart +++ b/lib/data/models/settings_model.dart @@ -369,6 +369,18 @@ abstract class SettingsEntity @BuiltValueField(wireName: 'credit_design_id') String? get defaultCreditDesignId; + @BuiltValueField(wireName: 'delivery_note_design_id') + String? get defaultDeliveryNoteDesignId; + + @BuiltValueField(wireName: 'statement_design_id') + String? get defaultStatementDesignId; + + @BuiltValueField(wireName: 'payment_receipt_design_id') + String? get defaultPaymentReceiptDesignId; + + @BuiltValueField(wireName: 'payment_refund_design_id') + String? get defaultPaymentRefundDesignId; + @BuiltValueField(wireName: 'invoice_footer') String? get defaultInvoiceFooter; diff --git a/lib/data/models/settings_model.g.dart b/lib/data/models/settings_model.g.dart index cfc995266..54822b1bf 100644 --- a/lib/data/models/settings_model.g.dart +++ b/lib/data/models/settings_model.g.dart @@ -559,6 +559,34 @@ class _$SettingsEntitySerializer ..add(serializers.serialize(value, specifiedType: const FullType(String))); } + value = object.defaultDeliveryNoteDesignId; + if (value != null) { + result + ..add('delivery_note_design_id') + ..add(serializers.serialize(value, + specifiedType: const FullType(String))); + } + value = object.defaultStatementDesignId; + if (value != null) { + result + ..add('statement_design_id') + ..add(serializers.serialize(value, + specifiedType: const FullType(String))); + } + value = object.defaultPaymentReceiptDesignId; + if (value != null) { + result + ..add('payment_receipt_design_id') + ..add(serializers.serialize(value, + specifiedType: const FullType(String))); + } + value = object.defaultPaymentRefundDesignId; + if (value != null) { + result + ..add('payment_refund_design_id') + ..add(serializers.serialize(value, + specifiedType: const FullType(String))); + } value = object.defaultInvoiceFooter; if (value != null) { result @@ -1873,6 +1901,22 @@ class _$SettingsEntitySerializer result.defaultCreditDesignId = serializers.deserialize(value, specifiedType: const FullType(String)) as String?; break; + case 'delivery_note_design_id': + result.defaultDeliveryNoteDesignId = serializers.deserialize(value, + specifiedType: const FullType(String)) as String?; + break; + case 'statement_design_id': + result.defaultStatementDesignId = serializers.deserialize(value, + specifiedType: const FullType(String)) as String?; + break; + case 'payment_receipt_design_id': + result.defaultPaymentReceiptDesignId = serializers.deserialize(value, + specifiedType: const FullType(String)) as String?; + break; + case 'payment_refund_design_id': + result.defaultPaymentRefundDesignId = serializers.deserialize(value, + specifiedType: const FullType(String)) as String?; + break; case 'invoice_footer': result.defaultInvoiceFooter = serializers.deserialize(value, specifiedType: const FullType(String)) as String?; @@ -2677,6 +2721,14 @@ class _$SettingsEntity extends SettingsEntity { @override final String? defaultCreditDesignId; @override + final String? defaultDeliveryNoteDesignId; + @override + final String? defaultStatementDesignId; + @override + final String? defaultPaymentReceiptDesignId; + @override + final String? defaultPaymentRefundDesignId; + @override final String? defaultInvoiceFooter; @override final String? defaultTaxName1; @@ -3041,6 +3093,10 @@ class _$SettingsEntity extends SettingsEntity { this.defaultInvoiceDesignId, this.defaultQuoteDesignId, this.defaultCreditDesignId, + this.defaultDeliveryNoteDesignId, + this.defaultStatementDesignId, + this.defaultPaymentReceiptDesignId, + this.defaultPaymentRefundDesignId, this.defaultInvoiceFooter, this.defaultTaxName1, this.defaultTaxRate1, @@ -3274,6 +3330,10 @@ class _$SettingsEntity extends SettingsEntity { defaultInvoiceDesignId == other.defaultInvoiceDesignId && defaultQuoteDesignId == other.defaultQuoteDesignId && defaultCreditDesignId == other.defaultCreditDesignId && + defaultDeliveryNoteDesignId == other.defaultDeliveryNoteDesignId && + defaultStatementDesignId == other.defaultStatementDesignId && + defaultPaymentReceiptDesignId == other.defaultPaymentReceiptDesignId && + defaultPaymentRefundDesignId == other.defaultPaymentRefundDesignId && defaultInvoiceFooter == other.defaultInvoiceFooter && defaultTaxName1 == other.defaultTaxName1 && defaultTaxRate1 == other.defaultTaxRate1 && @@ -3505,6 +3565,10 @@ class _$SettingsEntity extends SettingsEntity { _$hash = $jc(_$hash, defaultInvoiceDesignId.hashCode); _$hash = $jc(_$hash, defaultQuoteDesignId.hashCode); _$hash = $jc(_$hash, defaultCreditDesignId.hashCode); + _$hash = $jc(_$hash, defaultDeliveryNoteDesignId.hashCode); + _$hash = $jc(_$hash, defaultStatementDesignId.hashCode); + _$hash = $jc(_$hash, defaultPaymentReceiptDesignId.hashCode); + _$hash = $jc(_$hash, defaultPaymentRefundDesignId.hashCode); _$hash = $jc(_$hash, defaultInvoiceFooter.hashCode); _$hash = $jc(_$hash, defaultTaxName1.hashCode); _$hash = $jc(_$hash, defaultTaxRate1.hashCode); @@ -3731,6 +3795,10 @@ class _$SettingsEntity extends SettingsEntity { ..add('defaultInvoiceDesignId', defaultInvoiceDesignId) ..add('defaultQuoteDesignId', defaultQuoteDesignId) ..add('defaultCreditDesignId', defaultCreditDesignId) + ..add('defaultDeliveryNoteDesignId', defaultDeliveryNoteDesignId) + ..add('defaultStatementDesignId', defaultStatementDesignId) + ..add('defaultPaymentReceiptDesignId', defaultPaymentReceiptDesignId) + ..add('defaultPaymentRefundDesignId', defaultPaymentRefundDesignId) ..add('defaultInvoiceFooter', defaultInvoiceFooter) ..add('defaultTaxName1', defaultTaxName1) ..add('defaultTaxRate1', defaultTaxRate1) @@ -4268,6 +4336,29 @@ class SettingsEntityBuilder set defaultCreditDesignId(String? defaultCreditDesignId) => _$this._defaultCreditDesignId = defaultCreditDesignId; + String? _defaultDeliveryNoteDesignId; + String? get defaultDeliveryNoteDesignId => + _$this._defaultDeliveryNoteDesignId; + set defaultDeliveryNoteDesignId(String? defaultDeliveryNoteDesignId) => + _$this._defaultDeliveryNoteDesignId = defaultDeliveryNoteDesignId; + + String? _defaultStatementDesignId; + String? get defaultStatementDesignId => _$this._defaultStatementDesignId; + set defaultStatementDesignId(String? defaultStatementDesignId) => + _$this._defaultStatementDesignId = defaultStatementDesignId; + + String? _defaultPaymentReceiptDesignId; + String? get defaultPaymentReceiptDesignId => + _$this._defaultPaymentReceiptDesignId; + set defaultPaymentReceiptDesignId(String? defaultPaymentReceiptDesignId) => + _$this._defaultPaymentReceiptDesignId = defaultPaymentReceiptDesignId; + + String? _defaultPaymentRefundDesignId; + String? get defaultPaymentRefundDesignId => + _$this._defaultPaymentRefundDesignId; + set defaultPaymentRefundDesignId(String? defaultPaymentRefundDesignId) => + _$this._defaultPaymentRefundDesignId = defaultPaymentRefundDesignId; + String? _defaultInvoiceFooter; String? get defaultInvoiceFooter => _$this._defaultInvoiceFooter; set defaultInvoiceFooter(String? defaultInvoiceFooter) => @@ -5047,6 +5138,10 @@ class SettingsEntityBuilder _defaultInvoiceDesignId = $v.defaultInvoiceDesignId; _defaultQuoteDesignId = $v.defaultQuoteDesignId; _defaultCreditDesignId = $v.defaultCreditDesignId; + _defaultDeliveryNoteDesignId = $v.defaultDeliveryNoteDesignId; + _defaultStatementDesignId = $v.defaultStatementDesignId; + _defaultPaymentReceiptDesignId = $v.defaultPaymentReceiptDesignId; + _defaultPaymentRefundDesignId = $v.defaultPaymentRefundDesignId; _defaultInvoiceFooter = $v.defaultInvoiceFooter; _defaultTaxName1 = $v.defaultTaxName1; _defaultTaxRate1 = $v.defaultTaxRate1; @@ -5290,6 +5385,10 @@ class SettingsEntityBuilder defaultInvoiceDesignId: defaultInvoiceDesignId, defaultQuoteDesignId: defaultQuoteDesignId, defaultCreditDesignId: defaultCreditDesignId, + defaultDeliveryNoteDesignId: defaultDeliveryNoteDesignId, + defaultStatementDesignId: defaultStatementDesignId, + defaultPaymentReceiptDesignId: defaultPaymentReceiptDesignId, + defaultPaymentRefundDesignId: defaultPaymentRefundDesignId, defaultInvoiceFooter: defaultInvoiceFooter, defaultTaxName1: defaultTaxName1, defaultTaxRate1: defaultTaxRate1, diff --git a/lib/ui/app/forms/design_picker.dart b/lib/ui/app/forms/design_picker.dart index cf7c5ecb7..fc0c5c62a 100644 --- a/lib/ui/app/forms/design_picker.dart +++ b/lib/ui/app/forms/design_picker.dart @@ -15,11 +15,13 @@ class DesignPicker extends StatelessWidget { required this.onSelected, this.label, this.initialValue, + this.showBlank = false, }); - final Function(DesignEntity) onSelected; + final Function(DesignEntity?) onSelected; final String? label; final String? initialValue; + final bool showBlank; @override Widget build(BuildContext context) { @@ -29,6 +31,7 @@ class DesignPicker extends StatelessWidget { final designState = state.designState; return AppDropdownButton( + showBlank: showBlank, value: initialValue, onChanged: (dynamic value) => onSelected(designState.map[value]!), items: designState.list diff --git a/lib/ui/design/edit/design_edit.dart b/lib/ui/design/edit/design_edit.dart index d69eb060e..d0bc56351 100644 --- a/lib/ui/design/edit/design_edit.dart +++ b/lib/ui/design/edit/design_edit.dart @@ -462,7 +462,7 @@ class _DesignSettingsState extends State { DesignPicker( label: localization.design, onSelected: (value) { - widget.onLoadDesign(value); + widget.onLoadDesign(value!); _selectedDesign = value; }, initialValue: _selectedDesign?.id), diff --git a/lib/ui/invoice/edit/invoice_edit_desktop.dart b/lib/ui/invoice/edit/invoice_edit_desktop.dart index 9120d5684..ee6e659de 100644 --- a/lib/ui/invoice/edit/invoice_edit_desktop.dart +++ b/lib/ui/invoice/edit/invoice_edit_desktop.dart @@ -795,7 +795,7 @@ class InvoiceEditDesktopState extends State initialValue: invoice.designId, onSelected: (value) { viewModel.onChanged!(invoice.rebuild( - (b) => b..designId = value.id)); + (b) => b..designId = value!.id)); }, ), UserPicker( diff --git a/lib/ui/invoice/edit/invoice_edit_details.dart b/lib/ui/invoice/edit/invoice_edit_details.dart index d382f1a93..2c0930b3e 100644 --- a/lib/ui/invoice/edit/invoice_edit_details.dart +++ b/lib/ui/invoice/edit/invoice_edit_details.dart @@ -459,7 +459,7 @@ class InvoiceEditDetailsState extends State { DesignPicker( initialValue: invoice.designId, onSelected: (value) => viewModel - .onChanged!(invoice.rebuild((b) => b..designId = value.id)), + .onChanged!(invoice.rebuild((b) => b..designId = value!.id)), ), if (company.isModuleEnabled(EntityType.project)) ProjectPicker( diff --git a/lib/ui/settings/company_details.dart b/lib/ui/settings/company_details.dart index 3bf0f6a6c..a976afe14 100644 --- a/lib/ui/settings/company_details.dart +++ b/lib/ui/settings/company_details.dart @@ -664,7 +664,7 @@ class _CompanyDetailsState extends State initialValue: settings.defaultInvoiceDesignId, onSelected: (value) => viewModel.onSettingsChanged( settings.rebuild( - (b) => b..defaultInvoiceDesignId = value.id)), + (b) => b..defaultInvoiceDesignId = value!.id)), ), if (company.isModuleEnabled(EntityType.quote)) DesignPicker( @@ -672,7 +672,7 @@ class _CompanyDetailsState extends State initialValue: settings.defaultQuoteDesignId, onSelected: (value) => viewModel.onSettingsChanged( settings.rebuild( - (b) => b..defaultQuoteDesignId = value.id)), + (b) => b..defaultQuoteDesignId = value!.id)), ), if (company.isModuleEnabled(EntityType.credit)) DesignPicker( @@ -680,7 +680,7 @@ class _CompanyDetailsState extends State initialValue: settings.defaultCreditDesignId, onSelected: (value) => viewModel.onSettingsChanged( settings.rebuild( - (b) => b..defaultCreditDesignId = value.id)), + (b) => b..defaultCreditDesignId = value!.id)), ), if (company.isModuleEnabled(EntityType.purchaseOrder)) DesignPicker( @@ -688,7 +688,7 @@ class _CompanyDetailsState extends State initialValue: settings.defaultPurchaseOrderDesignId, onSelected: (value) => viewModel.onSettingsChanged( settings.rebuild((b) => - b..defaultPurchaseOrderDesignId = value.id)), + b..defaultPurchaseOrderDesignId = value!.id)), ), ]), if (!state.settingsUIState.isFiltered) diff --git a/lib/ui/settings/invoice_design.dart b/lib/ui/settings/invoice_design.dart index 06e5e6710..bb899af00 100644 --- a/lib/ui/settings/invoice_design.dart +++ b/lib/ui/settings/invoice_design.dart @@ -292,7 +292,7 @@ class _InvoiceDesignState extends State }); viewModel.onSettingsChanged(settings.rebuild( (b) => - b..defaultInvoiceDesignId = value.id)); + b..defaultInvoiceDesignId = value!.id)); }, ), if (!isFiltered && @@ -320,7 +320,8 @@ class _InvoiceDesignState extends State _wasQuoteDesignChanged = true; }); viewModel.onSettingsChanged(settings.rebuild( - (b) => b..defaultQuoteDesignId = value.id)); + (b) => + b..defaultQuoteDesignId = value!.id)); }, ), if (!isFiltered && @@ -349,7 +350,7 @@ class _InvoiceDesignState extends State }); viewModel.onSettingsChanged(settings.rebuild( (b) => - b..defaultCreditDesignId = value.id)); + b..defaultCreditDesignId = value!.id)); }, ), if (!isFiltered && @@ -381,7 +382,7 @@ class _InvoiceDesignState extends State viewModel.onSettingsChanged(settings.rebuild( (b) => b ..defaultPurchaseOrderDesignId = - value.id)); + value!.id)); }, ), if (!isFiltered && @@ -415,6 +416,21 @@ class _InvoiceDesignState extends State ), SizedBox(height: 16), ], + DesignPicker( + showBlank: true, + label: localization.deliveryNoteDesign, + initialValue: settings.defaultDeliveryNoteDesignId, + onSelected: (value) { + viewModel.onSettingsChanged(settings.rebuild((b) => + b + ..defaultDeliveryNoteDesignId = + value?.id ?? '')); + }, + ), + ], + ), + FormCard( + children: [ AppDropdownButton( labelText: localization.pageLayout, value: settings.pageLayout, diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index 723bbbe9e..60400a2c7 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -18,6 +18,10 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'delivery_note_design': 'Delivery Note Design', + 'statement_design': 'Statement Design', + 'payment_receipt_design': 'Payment Receipt Design', + 'payment_refund_design': 'Payment Refund Design', 'quarter': 'Quarter', 'item_description': 'Item Description', 'task_item': 'Task Item', @@ -109945,6 +109949,21 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]!['quarter'] ?? _localizedValues['en']!['quarter']!; + String get deliveryNoteDesign => + _localizedValues[localeCode]!['delivery_note_design'] ?? + _localizedValues['en']!['delivery_note_design']!; + + String get statementDesign => + _localizedValues[localeCode]!['statement_design'] ?? + _localizedValues['en']!['statement_design']!; + + String get paymentReceiptDesign => + _localizedValues[localeCode]!['payment_receipt_design'] ?? + _localizedValues['en']!['payment_receipt_design']!; + + String get paymentRefundDesign => + _localizedValues[localeCode]!['payment_refund_design'] ?? + _localizedValues['en']!['payment_refund_design']!; // STARTER: lang field - do not remove comment From eaacd9cbae224a227c5bc29c093e73e0ccc237d0 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 6 Nov 2023 17:08:16 +0200 Subject: [PATCH 084/138] Design template changes --- lib/ui/app/forms/design_picker.dart | 2 +- lib/ui/settings/invoice_design.dart | 31 ++++++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/lib/ui/app/forms/design_picker.dart b/lib/ui/app/forms/design_picker.dart index fc0c5c62a..115e2d7d4 100644 --- a/lib/ui/app/forms/design_picker.dart +++ b/lib/ui/app/forms/design_picker.dart @@ -33,7 +33,7 @@ class DesignPicker extends StatelessWidget { return AppDropdownButton( showBlank: showBlank, value: initialValue, - onChanged: (dynamic value) => onSelected(designState.map[value]!), + onChanged: (dynamic value) => onSelected(designState.map[value]), items: designState.list .where((designId) { final design = designState.map[designId]; diff --git a/lib/ui/settings/invoice_design.dart b/lib/ui/settings/invoice_design.dart index bb899af00..ad2e107d1 100644 --- a/lib/ui/settings/invoice_design.dart +++ b/lib/ui/settings/invoice_design.dart @@ -422,9 +422,34 @@ class _InvoiceDesignState extends State initialValue: settings.defaultDeliveryNoteDesignId, onSelected: (value) { viewModel.onSettingsChanged(settings.rebuild((b) => - b - ..defaultDeliveryNoteDesignId = - value?.id ?? '')); + b..defaultDeliveryNoteDesignId = value?.id)); + }, + ), + DesignPicker( + showBlank: true, + label: localization.statementDesign, + initialValue: settings.defaultStatementDesignId, + onSelected: (value) { + viewModel.onSettingsChanged(settings.rebuild((b) => + b..defaultStatementDesignId = value?.id)); + }, + ), + DesignPicker( + showBlank: true, + label: localization.paymentReceiptDesign, + initialValue: settings.defaultPaymentReceiptDesignId, + onSelected: (value) { + viewModel.onSettingsChanged(settings.rebuild((b) => + b..defaultPaymentReceiptDesignId = value?.id)); + }, + ), + DesignPicker( + showBlank: true, + label: localization.paymentRefundDesign, + initialValue: settings.defaultPaymentRefundDesignId, + onSelected: (value) { + viewModel.onSettingsChanged(settings.rebuild((b) => + b..defaultPaymentRefundDesignId = value?.id)); }, ), ], From f45a31d889a6e0a0ccf69e3831adca641e246479 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 6 Nov 2023 17:15:24 +0200 Subject: [PATCH 085/138] Design template changes --- lib/data/models/design_model.dart | 13 ++++- lib/data/models/design_model.g.dart | 44 ++++++++++++++++ lib/ui/settings/invoice_design.dart | 81 ++++++++++++++++------------- lib/ui/settings/settings_list.dart | 8 ++- lib/utils/platforms.dart | 2 + 5 files changed, 109 insertions(+), 39 deletions(-) diff --git a/lib/data/models/design_model.dart b/lib/data/models/design_model.dart index 3bec1ac53..aa3e4fa9d 100644 --- a/lib/data/models/design_model.dart +++ b/lib/data/models/design_model.dart @@ -112,6 +112,8 @@ abstract class DesignEntity extends Object kDesignIncludes: '', }), isCustom: true, + isTemplate: false, + entities: '', ); } @@ -134,6 +136,11 @@ abstract class DesignEntity extends Object @BuiltValueField(wireName: 'is_free') bool get isFree; + @BuiltValueField(wireName: 'is_template') + bool get isTemplate; + + String get entities; + DesignEntity get clone => rebuild((b) => b ..id = BaseEntity.nextId ..isChanged = false @@ -225,8 +232,10 @@ abstract class DesignEntity extends Object FormatNumberType? get listDisplayAmountType => null; // ignore: unused_element - static void _initializeBuilder(DesignEntityBuilder builder) => - builder..isFree = true; + static void _initializeBuilder(DesignEntityBuilder builder) => builder + ..isFree = true + ..isTemplate = false + ..entities = ''; static Serializer get serializer => _$designEntitySerializer; } diff --git a/lib/data/models/design_model.g.dart b/lib/data/models/design_model.g.dart index 782e87e40..8c8201d13 100644 --- a/lib/data/models/design_model.g.dart +++ b/lib/data/models/design_model.g.dart @@ -185,6 +185,12 @@ class _$DesignEntitySerializer implements StructuredSerializer { specifiedType: const FullType(bool)), 'is_free', serializers.serialize(object.isFree, specifiedType: const FullType(bool)), + 'is_template', + serializers.serialize(object.isTemplate, + specifiedType: const FullType(bool)), + 'entities', + serializers.serialize(object.entities, + specifiedType: const FullType(String)), 'created_at', serializers.serialize(object.createdAt, specifiedType: const FullType(int)), @@ -258,6 +264,14 @@ class _$DesignEntitySerializer implements StructuredSerializer { result.isFree = serializers.deserialize(value, specifiedType: const FullType(bool))! as bool; break; + case 'is_template': + result.isTemplate = serializers.deserialize(value, + specifiedType: const FullType(bool))! as bool; + break; + case 'entities': + result.entities = serializers.deserialize(value, + specifiedType: const FullType(String))! as String; + break; case 'isChanged': result.isChanged = serializers.deserialize(value, specifiedType: const FullType(bool)) as bool?; @@ -637,6 +651,10 @@ class _$DesignEntity extends DesignEntity { @override final bool isFree; @override + final bool isTemplate; + @override + final String entities; + @override final bool? isChanged; @override final int createdAt; @@ -661,6 +679,8 @@ class _$DesignEntity extends DesignEntity { required this.design, required this.isCustom, required this.isFree, + required this.isTemplate, + required this.entities, this.isChanged, required this.createdAt, required this.updatedAt, @@ -675,6 +695,10 @@ class _$DesignEntity extends DesignEntity { BuiltValueNullFieldError.checkNotNull( isCustom, r'DesignEntity', 'isCustom'); BuiltValueNullFieldError.checkNotNull(isFree, r'DesignEntity', 'isFree'); + BuiltValueNullFieldError.checkNotNull( + isTemplate, r'DesignEntity', 'isTemplate'); + BuiltValueNullFieldError.checkNotNull( + entities, r'DesignEntity', 'entities'); BuiltValueNullFieldError.checkNotNull( createdAt, r'DesignEntity', 'createdAt'); BuiltValueNullFieldError.checkNotNull( @@ -699,6 +723,8 @@ class _$DesignEntity extends DesignEntity { design == other.design && isCustom == other.isCustom && isFree == other.isFree && + isTemplate == other.isTemplate && + entities == other.entities && isChanged == other.isChanged && createdAt == other.createdAt && updatedAt == other.updatedAt && @@ -718,6 +744,8 @@ class _$DesignEntity extends DesignEntity { _$hash = $jc(_$hash, design.hashCode); _$hash = $jc(_$hash, isCustom.hashCode); _$hash = $jc(_$hash, isFree.hashCode); + _$hash = $jc(_$hash, isTemplate.hashCode); + _$hash = $jc(_$hash, entities.hashCode); _$hash = $jc(_$hash, isChanged.hashCode); _$hash = $jc(_$hash, createdAt.hashCode); _$hash = $jc(_$hash, updatedAt.hashCode); @@ -737,6 +765,8 @@ class _$DesignEntity extends DesignEntity { ..add('design', design) ..add('isCustom', isCustom) ..add('isFree', isFree) + ..add('isTemplate', isTemplate) + ..add('entities', entities) ..add('isChanged', isChanged) ..add('createdAt', createdAt) ..add('updatedAt', updatedAt) @@ -770,6 +800,14 @@ class DesignEntityBuilder bool? get isFree => _$this._isFree; set isFree(bool? isFree) => _$this._isFree = isFree; + bool? _isTemplate; + bool? get isTemplate => _$this._isTemplate; + set isTemplate(bool? isTemplate) => _$this._isTemplate = isTemplate; + + String? _entities; + String? get entities => _$this._entities; + set entities(String? entities) => _$this._entities = entities; + bool? _isChanged; bool? get isChanged => _$this._isChanged; set isChanged(bool? isChanged) => _$this._isChanged = isChanged; @@ -815,6 +853,8 @@ class DesignEntityBuilder _design = $v.design.toBuilder(); _isCustom = $v.isCustom; _isFree = $v.isFree; + _isTemplate = $v.isTemplate; + _entities = $v.entities; _isChanged = $v.isChanged; _createdAt = $v.createdAt; _updatedAt = $v.updatedAt; @@ -854,6 +894,10 @@ class DesignEntityBuilder isCustom, r'DesignEntity', 'isCustom'), isFree: BuiltValueNullFieldError.checkNotNull( isFree, r'DesignEntity', 'isFree'), + isTemplate: BuiltValueNullFieldError.checkNotNull( + isTemplate, r'DesignEntity', 'isTemplate'), + entities: BuiltValueNullFieldError.checkNotNull( + entities, r'DesignEntity', 'entities'), isChanged: isChanged, createdAt: BuiltValueNullFieldError.checkNotNull( createdAt, r'DesignEntity', 'createdAt'), diff --git a/lib/ui/settings/invoice_design.dart b/lib/ui/settings/invoice_design.dart index ad2e107d1..f4987ed9b 100644 --- a/lib/ui/settings/invoice_design.dart +++ b/lib/ui/settings/invoice_design.dart @@ -416,42 +416,51 @@ class _InvoiceDesignState extends State ), SizedBox(height: 16), ], - DesignPicker( - showBlank: true, - label: localization.deliveryNoteDesign, - initialValue: settings.defaultDeliveryNoteDesignId, - onSelected: (value) { - viewModel.onSettingsChanged(settings.rebuild((b) => - b..defaultDeliveryNoteDesignId = value?.id)); - }, - ), - DesignPicker( - showBlank: true, - label: localization.statementDesign, - initialValue: settings.defaultStatementDesignId, - onSelected: (value) { - viewModel.onSettingsChanged(settings.rebuild((b) => - b..defaultStatementDesignId = value?.id)); - }, - ), - DesignPicker( - showBlank: true, - label: localization.paymentReceiptDesign, - initialValue: settings.defaultPaymentReceiptDesignId, - onSelected: (value) { - viewModel.onSettingsChanged(settings.rebuild((b) => - b..defaultPaymentReceiptDesignId = value?.id)); - }, - ), - DesignPicker( - showBlank: true, - label: localization.paymentRefundDesign, - initialValue: settings.defaultPaymentRefundDesignId, - onSelected: (value) { - viewModel.onSettingsChanged(settings.rebuild((b) => - b..defaultPaymentRefundDesignId = value?.id)); - }, - ), + if (supportsDesignTemplates()) ...[ + DesignPicker( + showBlank: true, + label: localization.deliveryNoteDesign, + initialValue: settings.defaultDeliveryNoteDesignId, + onSelected: (value) { + viewModel.onSettingsChanged(settings.rebuild( + (b) => b + ..defaultDeliveryNoteDesignId = value?.id)); + }, + ), + DesignPicker( + showBlank: true, + label: localization.statementDesign, + initialValue: settings.defaultStatementDesignId, + onSelected: (value) { + viewModel.onSettingsChanged(settings.rebuild( + (b) => + b..defaultStatementDesignId = value?.id)); + }, + ), + DesignPicker( + showBlank: true, + label: localization.paymentReceiptDesign, + initialValue: + settings.defaultPaymentReceiptDesignId, + onSelected: (value) { + viewModel.onSettingsChanged(settings.rebuild( + (b) => b + ..defaultPaymentReceiptDesignId = + value?.id)); + }, + ), + DesignPicker( + showBlank: true, + label: localization.paymentRefundDesign, + initialValue: settings.defaultPaymentRefundDesignId, + onSelected: (value) { + viewModel.onSettingsChanged(settings.rebuild( + (b) => b + ..defaultPaymentRefundDesignId = + value?.id)); + }, + ), + ], ], ), FormCard( diff --git a/lib/ui/settings/settings_list.dart b/lib/ui/settings/settings_list.dart index 4c6f3ff28..81e2d049d 100644 --- a/lib/ui/settings/settings_list.dart +++ b/lib/ui/settings/settings_list.dart @@ -550,7 +550,13 @@ class SettingsSearch extends StatelessWidget { 'show_paid_stamp#2023-01-29', 'show_shipping_address#2023-01-29', 'share_invoice_quote_columns#2023-03-20', - 'invoice_embed_documents#2023-10-27' + 'invoice_embed_documents#2023-10-27', + if (supportsDesignTemplates()) ...[ + 'delivery_note_design', + 'statement_design', + 'payment_receipt_design', + 'payment_refund_design', + ], ], ], kSettingsCustomDesigns: [ diff --git a/lib/utils/platforms.dart b/lib/utils/platforms.dart index b9561a870..02e448b8c 100644 --- a/lib/utils/platforms.dart +++ b/lib/utils/platforms.dart @@ -47,6 +47,8 @@ bool supportsAppleOAuth() => kIsWeb || isApple(); // TODO remove this function bool supportsMicrosoftOAuth() => kIsWeb; +bool supportsDesignTemplates() => !kReleaseMode; + bool supportsLatestFeatures(String version) { final store = StoreProvider.of(navigatorKey.currentContext!); final state = store.state; From adcd5b67714848905fea7e29a416c0b21f63438a Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 6 Nov 2023 17:16:41 +0200 Subject: [PATCH 086/138] Design template changes --- lib/ui/settings/settings_list.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/ui/settings/settings_list.dart b/lib/ui/settings/settings_list.dart index 81e2d049d..bf55e24d8 100644 --- a/lib/ui/settings/settings_list.dart +++ b/lib/ui/settings/settings_list.dart @@ -552,10 +552,10 @@ class SettingsSearch extends StatelessWidget { 'share_invoice_quote_columns#2023-03-20', 'invoice_embed_documents#2023-10-27', if (supportsDesignTemplates()) ...[ - 'delivery_note_design', - 'statement_design', - 'payment_receipt_design', - 'payment_refund_design', + 'delivery_note_design#2023-11-06', + 'statement_design#2023-11-06', + 'payment_receipt_design#2023-11-06', + 'payment_refund_design#2023-11-06', ], ], ], From 4b29fa2b0f70267779eb8303c9a944debfeb1bd9 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 6 Nov 2023 17:59:11 +0200 Subject: [PATCH 087/138] Design template changes --- lib/ui/design/edit/design_edit.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/ui/design/edit/design_edit.dart b/lib/ui/design/edit/design_edit.dart index d0bc56351..ff677a8f0 100644 --- a/lib/ui/design/edit/design_edit.dart +++ b/lib/ui/design/edit/design_edit.dart @@ -449,6 +449,7 @@ class _DesignSettingsState extends State { final localization = AppLocalization.of(context)!; return ScrollableListView( + primary: true, children: [ FormCard( children: [ @@ -466,9 +467,16 @@ class _DesignSettingsState extends State { _selectedDesign = value; }, initialValue: _selectedDesign?.id), + SizedBox(height: 16), + SwitchListTile( + activeColor: Theme.of(context).colorScheme.secondary, + title: Text(localization.template), + //subtitle: Text(localization.draftModeHelp), + value: widget.draftMode, + onChanged: widget.isLoading ? null : widget.onDraftModeChanged, + ), // TODO remove this once browser supported on all platforms - if (!kReleaseMode || kIsWeb || isMobileOS()) ...[ - SizedBox(height: 16), + if (!kReleaseMode || kIsWeb || isMobileOS()) SwitchListTile( activeColor: Theme.of(context).colorScheme.secondary, title: Text(localization.draftMode), @@ -476,7 +484,6 @@ class _DesignSettingsState extends State { value: widget.draftMode, onChanged: widget.isLoading ? null : widget.onDraftModeChanged, ), - ] ], ), Padding( From 6e1b9ae5c4bb14ce3816678055cf963f78fcb038 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 7 Nov 2023 12:56:50 +0200 Subject: [PATCH 088/138] Design template changes --- lib/ui/design/edit/design_edit.dart | 45 ++++++++++++++++++++++++----- lib/ui/settings/invoice_design.dart | 2 ++ lib/utils/i18n.dart | 5 ++++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/lib/ui/design/edit/design_edit.dart b/lib/ui/design/edit/design_edit.dart index ff677a8f0..afe447a6c 100644 --- a/lib/ui/design/edit/design_edit.dart +++ b/lib/ui/design/edit/design_edit.dart @@ -11,6 +11,7 @@ import 'package:flutter/services.dart'; import 'package:built_collection/built_collection.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; +import 'package:invoiceninja_flutter/data/models/entities.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/utils/dialogs.dart'; import 'package:printing/printing.dart'; @@ -447,6 +448,8 @@ class _DesignSettingsState extends State { @override Widget build(BuildContext context) { final localization = AppLocalization.of(context)!; + final design = widget.viewModel.design; + final entityTypes = design.entities.split(','); return ScrollableListView( primary: true, @@ -468,13 +471,6 @@ class _DesignSettingsState extends State { }, initialValue: _selectedDesign?.id), SizedBox(height: 16), - SwitchListTile( - activeColor: Theme.of(context).colorScheme.secondary, - title: Text(localization.template), - //subtitle: Text(localization.draftModeHelp), - value: widget.draftMode, - onChanged: widget.isLoading ? null : widget.onDraftModeChanged, - ), // TODO remove this once browser supported on all platforms if (!kReleaseMode || kIsWeb || isMobileOS()) SwitchListTile( @@ -484,6 +480,41 @@ class _DesignSettingsState extends State { value: widget.draftMode, onChanged: widget.isLoading ? null : widget.onDraftModeChanged, ), + if (supportsDesignTemplates()) + SwitchListTile( + activeColor: Theme.of(context).colorScheme.secondary, + title: Text(localization.template), + subtitle: Text(localization.templateHelp), + value: design.isTemplate, + onChanged: (value) { + widget.viewModel.onChanged( + design.rebuild((b) => b..isTemplate = value), + ); + }, + ), + if (design.isTemplate) ...[ + SizedBox(height: 10), + ...[ + EntityType.client, + EntityType.invoice, + ] + .map((entityType) => CheckboxListTile( + value: entityTypes.contains(entityType.apiValue), + onChanged: (value) { + final entities = entityTypes; + if (value == true) { + entities.add(entityType.apiValue); + } else { + entities.remove(entityType.apiValue); + } + widget.viewModel.onChanged(design.rebuild( + (b) => b..entities = entities.join(','))); + }, + title: Text(localization.lookup(entityType.plural)), + controlAffinity: ListTileControlAffinity.leading, + )) + .toList(), + ], ], ), Padding( diff --git a/lib/ui/settings/invoice_design.dart b/lib/ui/settings/invoice_design.dart index f4987ed9b..6e9397da6 100644 --- a/lib/ui/settings/invoice_design.dart +++ b/lib/ui/settings/invoice_design.dart @@ -449,6 +449,7 @@ class _InvoiceDesignState extends State value?.id)); }, ), + /* DesignPicker( showBlank: true, label: localization.paymentRefundDesign, @@ -460,6 +461,7 @@ class _InvoiceDesignState extends State value?.id)); }, ), + */ ], ], ), diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index 60400a2c7..f88f95f22 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -18,6 +18,7 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'template_help': 'Enable using the design as a template', 'delivery_note_design': 'Delivery Note Design', 'statement_design': 'Statement Design', 'payment_receipt_design': 'Payment Receipt Design', @@ -109965,6 +109966,10 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]!['payment_refund_design'] ?? _localizedValues['en']!['payment_refund_design']!; + String get templateHelp => + _localizedValues[localeCode]!['template_help'] ?? + _localizedValues['en']!['template_help']!; + // STARTER: lang field - do not remove comment String lookup(String? key) { From bcd84f13435a940c06096467ef7adecb07fb0b05 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 7 Nov 2023 13:08:09 +0200 Subject: [PATCH 089/138] Design template changes --- lib/ui/design/design_list_item.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/ui/design/design_list_item.dart b/lib/ui/design/design_list_item.dart index f96f2fb29..1ed91474d 100644 --- a/lib/ui/design/design_list_item.dart +++ b/lib/ui/design/design_list_item.dart @@ -11,7 +11,7 @@ import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/ui/app/dismissible_entity.dart'; import 'package:invoiceninja_flutter/ui/app/entity_state_label.dart'; -import 'package:invoiceninja_flutter/utils/formatting.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; class DesignListItem extends StatelessWidget { const DesignListItem({ @@ -75,8 +75,9 @@ class DesignListItem extends StatelessWidget { style: Theme.of(context).textTheme.titleMedium, ), ), - Text(formatNumber(design.listDisplayAmount, context)!, - style: Theme.of(context).textTheme.titleMedium), + if (design.isTemplate) + Text(AppLocalization.of(context)!.template, + style: Theme.of(context).textTheme.bodySmall), ], ), ), From 1d435e7fc65a949ebefff5841715ed0622cffef4 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 7 Nov 2023 17:41:02 +0200 Subject: [PATCH 090/138] Fix for invoice history list --- lib/ui/invoice/view/invoice_view_history.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ui/invoice/view/invoice_view_history.dart b/lib/ui/invoice/view/invoice_view_history.dart index dd19c762d..4ec68fa85 100644 --- a/lib/ui/invoice/view/invoice_view_history.dart +++ b/lib/ui/invoice/view/invoice_view_history.dart @@ -44,7 +44,7 @@ class _InvoiceViewHistoryState extends State { } final activityList = invoice.activities - .where((activity) => activity.history != null) + .where((activity) => (activity.history?.id ?? '').isNotEmpty) .toList(); activityList.sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); From 44de6a6980d3da0b1f5631ded8ad87958e17038b Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 7 Nov 2023 17:49:58 +0200 Subject: [PATCH 091/138] Fix for invoice history list --- lib/data/models/invoice_model.dart | 15 +++++++++++++++ lib/ui/invoice/invoice_pdf.dart | 2 +- lib/ui/invoice/view/invoice_view_history.dart | 9 ++++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/data/models/invoice_model.dart b/lib/data/models/invoice_model.dart index 569453427..8c5487de3 100644 --- a/lib/data/models/invoice_model.dart +++ b/lib/data/models/invoice_model.dart @@ -635,6 +635,21 @@ abstract class InvoiceEntity extends Object .whereType() .toList(); + List get balanceHistory => activities + .where((activity) => + activity.history != null && + activity.history!.id.isNotEmpty && + activity.history!.createdAt > 0 && + ![ + kActivityViewInvoice, + kActivityViewQuote, + kActivityViewCredit, + kActivityViewPurchaseOrder, + ].contains(activity.activityTypeId)) + .map((activity) => activity.history) + .whereType() + .toList(); + bool get isLoaded => loadedAt != null && loadedAt! > 0; bool get isStale { diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index ff0b15d3f..36b1be994 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -161,7 +161,7 @@ class _InvoicePdfViewState extends State { loadPdf(); }); }, - items: invoice.history + items: invoice.balanceHistory .map((history) => DropdownMenuItem( child: Text(formatNumber( history.amount, context, diff --git a/lib/ui/invoice/view/invoice_view_history.dart b/lib/ui/invoice/view/invoice_view_history.dart index 4ec68fa85..09815b15c 100644 --- a/lib/ui/invoice/view/invoice_view_history.dart +++ b/lib/ui/invoice/view/invoice_view_history.dart @@ -1,5 +1,6 @@ // Flutter imports: import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/redux/company/company_selectors.dart'; // Package imports: @@ -45,6 +46,12 @@ class _InvoiceViewHistoryState extends State { final activityList = invoice.activities .where((activity) => (activity.history?.id ?? '').isNotEmpty) + .where((activity) => ![ + kActivityViewInvoice, + kActivityViewQuote, + kActivityViewCredit, + kActivityViewPurchaseOrder, + ].contains(activity.activityTypeId)) .toList(); activityList.sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); @@ -97,7 +104,7 @@ class _InvoiceViewHistoryState extends State { ); }, separatorBuilder: (context, index) => ListDivider(), - itemCount: invoice.history.length, + itemCount: activityList.length, ); } } From 9f837bfaf6d92107e41c2c08caf28bc7e2c44812 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 7 Nov 2023 18:03:23 +0200 Subject: [PATCH 092/138] Fix for invoice history list --- lib/ui/invoice/invoice_pdf.dart | 253 ++++++++++++++++---------------- 1 file changed, 126 insertions(+), 127 deletions(-) diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index 36b1be994..381f2a9c3 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -180,29 +180,21 @@ class _InvoicePdfViewState extends State { ), ]; - final deliveryNote = Theme( - data: ThemeData( - unselectedWidgetColor: state.headerTextColor, - ), - child: Container( - width: 200, - child: CheckboxListTile( - title: Text( - localization.deliveryNote, - style: TextStyle( - color: state.headerTextColor, - ), - ), - value: _isDeliveryNote, - onChanged: (value) { - setState(() { - _isDeliveryNote = !_isDeliveryNote; - loadPdf(); - }); - }, - controlAffinity: ListTileControlAffinity.leading, - activeColor: state.accentColor, + final deliveryNote = Container( + width: 200, + child: CheckboxListTile( + title: Text( + localization.deliveryNote, ), + value: _isDeliveryNote, + onChanged: (value) { + setState(() { + _isDeliveryNote = !_isDeliveryNote; + loadPdf(); + }); + }, + controlAffinity: ListTileControlAffinity.leading, + activeColor: state.accentColor, ), ); @@ -216,116 +208,123 @@ class _InvoicePdfViewState extends State { } return Scaffold( - backgroundColor: Colors.grey.shade300, - appBar: widget.showAppBar - ? AppBar( - centerTitle: false, - automaticallyImplyLeading: isMobile(context), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text(EntityPresenter() - .initialize(invoice, context) - .title()!), - ), - if (isDesktop(context)) ...activitySelector, - //if (isDesktop(context)) ...pageSelector, - if (isDesktop(context) && - invoice.isInvoice && - _activityId == null) - deliveryNote, - ], - ), - actions: [ - if (showEmail) - TextButton( - child: Text(localization.email, - style: TextStyle(color: state.headerTextColor)), - onPressed: () { - handleEntityAction(invoice, EntityAction.sendEmail); - }, - ), - if (!invoice.isRecurring) - AppTextButton( - isInHeader: true, - label: localization.download, - onPressed: _response == null - ? null - : () async { - if (_response == null) { - launchUrl( - Uri.parse(invoice.invitationDownloadLink)); + backgroundColor: Colors.grey.shade300, + appBar: widget.showAppBar + ? AppBar( + centerTitle: false, + automaticallyImplyLeading: isMobile(context), + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text(EntityPresenter() + .initialize(invoice, context) + .title()!), + ), + if (isDesktop(context)) ...activitySelector, + ], + ), + actions: [ + if (showEmail) + TextButton( + child: Text(localization.email, + style: TextStyle(color: state.headerTextColor)), + onPressed: () { + handleEntityAction(invoice, EntityAction.sendEmail); + }, + ), + if (!invoice.isRecurring) + AppTextButton( + isInHeader: true, + label: localization.download, + onPressed: _response == null + ? null + : () async { + if (_response == null) { + launchUrl( + Uri.parse(invoice.invitationDownloadLink)); + } else { + final fileName = + localization.lookup('${invoice.entityType}') + + '_' + + (invoice.number.isEmpty + ? localization.pending + : invoice.number) + + '.pdf'; + if (kIsWeb) { + WebUtils.downloadBinaryFile( + fileName, _response!.bodyBytes); } else { - final fileName = localization - .lookup('${invoice.entityType}') + - '_' + - (invoice.number.isEmpty - ? localization.pending - : invoice.number) + - '.pdf'; - if (kIsWeb) { - WebUtils.downloadBinaryFile( - fileName, _response!.bodyBytes); + final directory = + await getAppDownloadDirectory(); + + if (directory == null) { + return; + } + + String filePath = + '$directory${file.Platform.pathSeparator}$fileName'; + + if (file.File(filePath).existsSync()) { + final timestamp = + DateTime.now().millisecondsSinceEpoch; + filePath = filePath.replaceFirst( + '.pdf', '_$timestamp.pdf'); + } + + final pdfData = file.File(filePath); + await pdfData + .writeAsBytes(_response!.bodyBytes); + + if (isDesktopOS()) { + showToast(localization.fileSavedInPath + .replaceFirst(':path', directory)); } else { - final directory = - await getAppDownloadDirectory(); - - if (directory == null) { - return; - } - - String filePath = - '$directory${file.Platform.pathSeparator}$fileName'; - - if (file.File(filePath).existsSync()) { - final timestamp = - DateTime.now().millisecondsSinceEpoch; - filePath = filePath.replaceFirst( - '.pdf', '_$timestamp.pdf'); - } - - final pdfData = file.File(filePath); - await pdfData - .writeAsBytes(_response!.bodyBytes); - - if (isDesktopOS()) { - showToast(localization.fileSavedInPath - .replaceFirst(':path', directory)); - } else { - await Share.shareXFiles([XFile(filePath)]); - } + await Share.shareXFiles([XFile(filePath)]); } } - }, + } + }, + ), + if (isDesktop(context)) + TextButton( + child: Text(localization.close, + style: TextStyle(color: state.headerTextColor)), + onPressed: () { + viewEntity(entity: invoice); + }, + ), + ], + ) + : null, + body: Column(children: [ + Material( + child: Row( + children: [ + if (invoice.isInvoice && _activityId == null) deliveryNote, + ], + ), + ), + Expanded( + child: _isLoading || _response == null + ? LoadingIndicator() + : (kIsWeb && state.prefState.enableNativeBrowser) + ? HtmlElementView(viewType: _pdfString!) + : PdfPreview( + build: (format) => _response!.bodyBytes, + canChangeOrientation: false, + canChangePageFormat: false, + canDebug: false, + maxPageWidth: 800, + pdfFileName: + localization.lookup(invoice.entityType!.snakeCase) + + '_' + + invoice.number + + '.pdf', ), - if (isDesktop(context)) - TextButton( - child: Text(localization.close, - style: TextStyle(color: state.headerTextColor)), - onPressed: () { - viewEntity(entity: invoice); - }, - ), - ], - ) - : null, - body: _isLoading || _response == null - ? LoadingIndicator() - : (kIsWeb && state.prefState.enableNativeBrowser) - ? HtmlElementView(viewType: _pdfString!) - : PdfPreview( - build: (format) => _response!.bodyBytes, - canChangeOrientation: false, - canChangePageFormat: false, - canDebug: false, - maxPageWidth: 800, - pdfFileName: - localization.lookup(invoice.entityType!.snakeCase) + - '_' + - invoice.number + - '.pdf', - )); + ), + ]), + ); } } From 4281c7a5a26864e5ba028b56074af06f9d8fb1d9 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 7 Nov 2023 18:13:33 +0200 Subject: [PATCH 093/138] Design template changes --- lib/ui/invoice/invoice_pdf.dart | 76 ++++++++++++++------------------- 1 file changed, 32 insertions(+), 44 deletions(-) diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index 381f2a9c3..8fbe24f23 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -144,41 +144,37 @@ class _InvoicePdfViewState extends State { */ final activitySelector = _activityId == null || kIsWeb - ? [] - : [ - Theme( - data: state.prefState.enableDarkMode || state.hasAccentColor - ? ThemeData.dark() - : ThemeData.light(), - child: Flexible( - child: IgnorePointer( - ignoring: _isLoading, - child: AppDropdownButton( - value: _activityId, - onChanged: (dynamic activityId) { - setState(() { - _activityId = activityId; - loadPdf(); - }); - }, - items: invoice.balanceHistory - .map((history) => DropdownMenuItem( - child: Text(formatNumber( - history.amount, context, - clientId: invoice.clientId)! + - ' • ' + - formatDate( - convertTimestampToDateString( - history.createdAt), - context, - showTime: true)), - value: history.activityId, - )) - .toList()), - ), + ? SizedBox() + : Padding( + padding: const EdgeInsets.only(left: 17), + child: SizedBox( + width: 350, + child: IgnorePointer( + ignoring: _isLoading, + child: AppDropdownButton( + value: _activityId, + onChanged: (dynamic activityId) { + setState(() { + _activityId = activityId; + loadPdf(); + }); + }, + items: invoice.balanceHistory + .map((history) => DropdownMenuItem( + child: Text(formatNumber(history.amount, context, + clientId: invoice.clientId)! + + ' • ' + + formatDate( + convertTimestampToDateString( + history.createdAt), + context, + showTime: true)), + value: history.activityId, + )) + .toList()), ), ), - ]; + ); final deliveryNote = Container( width: 200, @@ -213,17 +209,8 @@ class _InvoicePdfViewState extends State { ? AppBar( centerTitle: false, automaticallyImplyLeading: isMobile(context), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text(EntityPresenter() - .initialize(invoice, context) - .title()!), - ), - if (isDesktop(context)) ...activitySelector, - ], - ), + title: + Text(EntityPresenter().initialize(invoice, context).title()!), actions: [ if (showEmail) TextButton( @@ -302,6 +289,7 @@ class _InvoicePdfViewState extends State { child: Row( children: [ if (invoice.isInvoice && _activityId == null) deliveryNote, + activitySelector, ], ), ), From 39d06df0f420f95f2c0ea8a6d228ec609d5e30a8 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 8 Nov 2023 11:27:34 +0200 Subject: [PATCH 094/138] Show 'System' invoice history --- lib/ui/invoice/view/invoice_view_history.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/ui/invoice/view/invoice_view_history.dart b/lib/ui/invoice/view/invoice_view_history.dart index 09815b15c..bdf94270c 100644 --- a/lib/ui/invoice/view/invoice_view_history.dart +++ b/lib/ui/invoice/view/invoice_view_history.dart @@ -38,6 +38,7 @@ class _InvoiceViewHistoryState extends State { Widget build(BuildContext context) { final viewModel = widget.viewModel; final invoice = viewModel.invoice!; + final localization = AppLocalization.of(context)!; // TODO remove this null check, it shouldn't be needed if (invoice.isStale) { @@ -56,7 +57,7 @@ class _InvoiceViewHistoryState extends State { activityList.sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); if (activityList.isEmpty) { - return HelpText(AppLocalization.of(context)!.noHistory); + return HelpText(localization.noHistory); } return ScrollableListViewBuilder( @@ -71,7 +72,7 @@ class _InvoiceViewHistoryState extends State { final contact = client.getContact(activity.contactId); final user = state.userState.get(activity.userId); - String? personName; + String personName = ''; if (contact.isOld) { personName = contact.fullNameOrEmail; } else if (user.isOld) { @@ -83,6 +84,10 @@ class _InvoiceViewHistoryState extends State { personName = client.name; } + if (personName.isEmpty) { + personName = localization.system; + } + return ListTile( title: Text( formatNumber(history.amount, context, clientId: invoice.clientId)! + From 43599feb4409309b1adef8bdbda7f24453834179 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 8 Nov 2023 11:37:09 +0200 Subject: [PATCH 095/138] Send token for PDF download --- lib/redux/app/app_state.dart | 2 ++ lib/redux/credit/credit_actions.dart | 2 +- lib/redux/invoice/invoice_actions.dart | 2 +- lib/redux/purchase_order/purchase_order_actions.dart | 2 +- lib/redux/quote/quote_actions.dart | 2 +- lib/ui/invoice/invoice_pdf.dart | 6 ++++-- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/redux/app/app_state.dart b/lib/redux/app/app_state.dart index bb936deab..d2a12e2f6 100644 --- a/lib/redux/app/app_state.dart +++ b/lib/redux/app/app_state.dart @@ -173,6 +173,8 @@ abstract class AppState implements Built { UserCompanyEntity get userCompany => userCompanyState.userCompany; + String get token => userCompany.token.token; + Credentials get credentials => Credentials(token: userCompanyState.token.token, url: authState.url); diff --git a/lib/redux/credit/credit_actions.dart b/lib/redux/credit/credit_actions.dart index 620e5b7b7..b0d663638 100644 --- a/lib/redux/credit/credit_actions.dart +++ b/lib/redux/credit/credit_actions.dart @@ -678,7 +678,7 @@ Future handleCreditAction(BuildContext context, List credits, final url = invitation.downloadLink; store.dispatch(StartSaving()); final http.Response? response = - await WebClient().get(url, '', rawResponse: true); + await WebClient().get(url, state.token, rawResponse: true); store.dispatch(StopSaving()); await Printing.layoutPdf(onLayout: (_) => response!.bodyBytes); break; diff --git a/lib/redux/invoice/invoice_actions.dart b/lib/redux/invoice/invoice_actions.dart index 9c02e441e..08c554573 100644 --- a/lib/redux/invoice/invoice_actions.dart +++ b/lib/redux/invoice/invoice_actions.dart @@ -824,7 +824,7 @@ void handleInvoiceAction(BuildContext? context, List invoices, final url = invitation.downloadLink; store.dispatch(StartSaving()); final http.Response? response = - await WebClient().get(url, '', rawResponse: true); + await WebClient().get(url, state.token, rawResponse: true); store.dispatch(StopSaving()); await Printing.layoutPdf(onLayout: (_) => response!.bodyBytes); break; diff --git a/lib/redux/purchase_order/purchase_order_actions.dart b/lib/redux/purchase_order/purchase_order_actions.dart index cfd5b4d03..6574ee612 100644 --- a/lib/redux/purchase_order/purchase_order_actions.dart +++ b/lib/redux/purchase_order/purchase_order_actions.dart @@ -625,7 +625,7 @@ void handlePurchaseOrderAction(BuildContext? context, final url = invitation.downloadLink; store.dispatch(StartSaving()); final http.Response? response = - await WebClient().get(url, '', rawResponse: true); + await WebClient().get(url, state.token, rawResponse: true); store.dispatch(StopSaving()); await Printing.layoutPdf(onLayout: (_) => response!.bodyBytes); break; diff --git a/lib/redux/quote/quote_actions.dart b/lib/redux/quote/quote_actions.dart index c13551211..1def75eea 100644 --- a/lib/redux/quote/quote_actions.dart +++ b/lib/redux/quote/quote_actions.dart @@ -731,7 +731,7 @@ Future handleQuoteAction( final url = invitation.downloadLink; store.dispatch(StartSaving()); final http.Response? response = - await WebClient().get(url, '', rawResponse: true); + await WebClient().get(url, state.token, rawResponse: true); store.dispatch(StopSaving()); await Printing.layoutPdf(onLayout: (_) => response!.bodyBytes); break; diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index 8fbe24f23..89dd999aa 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -324,8 +324,10 @@ Future _loadPDF( ) async { http.Response? response; + final store = StoreProvider.of(context); + final state = store.state; + if ((activityId ?? '').isNotEmpty || isDeliveryNote) { - final store = StoreProvider.of(context); final credential = store.state.credentials; final url = isDeliveryNote ? '/invoices/${invoice.id}/delivery_note' @@ -335,7 +337,7 @@ Future _loadPDF( } else { final invitation = invoice.invitations.first; final url = invitation.downloadLink; - response = await WebClient().get(url, '', rawResponse: true); + response = await WebClient().get(url, state.token, rawResponse: true); } return response; From b5346e982b3489c5a4d8e82943f1d8c655f5214a Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 8 Nov 2023 13:20:56 +0200 Subject: [PATCH 096/138] Send token for PDF download --- lib/redux/document/document_actions.dart | 27 +------------------- lib/ui/client/client_pdf.dart | 28 +-------------------- lib/ui/invoice/invoice_pdf.dart | 32 ++---------------------- lib/utils/files.dart | 26 +++++++++++++++++++ 4 files changed, 30 insertions(+), 83 deletions(-) diff --git a/lib/redux/document/document_actions.dart b/lib/redux/document/document_actions.dart index 70e3ca54c..44ec4bc3d 100644 --- a/lib/redux/document/document_actions.dart +++ b/lib/redux/document/document_actions.dart @@ -1,7 +1,5 @@ // Dart imports: import 'dart:async'; -import 'dart:io' as file; -import 'dart:io'; // Flutter imports: import 'package:built_collection/built_collection.dart'; @@ -10,7 +8,6 @@ import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_redux/flutter_redux.dart'; -import 'package:flutter_styled_toast/flutter_styled_toast.dart'; // Project imports: import 'package:invoiceninja_flutter/data/models/models.dart'; @@ -36,13 +33,11 @@ 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/localization.dart'; -import 'package:invoiceninja_flutter/utils/platforms.dart'; import 'package:pinch_zoom/pinch_zoom.dart'; import 'package:printing/printing.dart'; import 'package:invoiceninja_flutter/utils/web_stub.dart' if (dart.library.html) 'package:invoiceninja_flutter/utils/web.dart'; -import 'package:share_plus/share_plus.dart'; class ViewDocumentList implements PersistUI { ViewDocumentList({ @@ -440,27 +435,7 @@ void handleDocumentAction( WebUtils.downloadBinaryFile(document!.name, document.data!); } } else { - final directory = await getAppDownloadDirectory(); - if (directory != null) { - String filePath = - '$directory/${file.Platform.pathSeparator}${document!.name}'; - - if (file.File(filePath).existsSync()) { - final extension = document.name.split('.').last; - final timestamp = DateTime.now().millisecondsSinceEpoch; - filePath = filePath.replaceFirst( - '.$extension', '_$timestamp.$extension'); - } - - await File(filePath).writeAsBytes(document.data!); - - if (isDesktopOS()) { - showToast(localization.fileSavedInPath - .replaceFirst(':path', directory)); - } else { - await Share.shareXFiles([XFile(filePath)]); - } - } + saveDownloadedFile(document!.data!, document.name); } } if (document.data == null) { diff --git a/lib/ui/client/client_pdf.dart b/lib/ui/client/client_pdf.dart index 6f1e5c365..48d20c734 100644 --- a/lib/ui/client/client_pdf.dart +++ b/lib/ui/client/client_pdf.dart @@ -1,7 +1,6 @@ // Dart imports: import 'dart:async'; import 'dart:convert'; -import 'dart:io' as file; // Flutter imports: import 'package:built_collection/built_collection.dart'; @@ -41,7 +40,6 @@ import 'package:invoiceninja_flutter/utils/platforms.dart'; import 'package:invoiceninja_flutter/utils/web_stub.dart' if (dart.library.html) 'package:invoiceninja_flutter/utils/web.dart'; -import 'package:share_plus/share_plus.dart'; class ClientPdfView extends StatefulWidget { const ClientPdfView({ @@ -339,31 +337,7 @@ class _ClientPdfViewState extends State { WebUtils.downloadBinaryFile( fileName, _response!.bodyBytes); } else { - final directory = await getAppDownloadDirectory(); - - if (directory == null) { - return; - } - - String filePath = - '$directory${file.Platform.pathSeparator}$fileName'; - - if (file.File(filePath).existsSync()) { - final timestamp = - DateTime.now().millisecondsSinceEpoch; - filePath = filePath.replaceFirst( - '.pdf', '_$timestamp.pdf'); - } - - final pdfData = file.File(filePath); - await pdfData.writeAsBytes(_response!.bodyBytes); - - if (isDesktopOS()) { - showToast(localization.fileSavedInPath - .replaceFirst(':path', directory)); - } else { - await Share.shareXFiles([XFile(filePath)]); - } + saveDownloadedFile(_response!.bodyBytes, fileName); } }, ), diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index 89dd999aa..0b302c4de 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -1,7 +1,6 @@ // Dart imports: import 'dart:async'; import 'dart:convert'; -import 'dart:io' as file; // Flutter imports: import 'package:flutter/foundation.dart'; @@ -9,14 +8,12 @@ import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_redux/flutter_redux.dart'; -import 'package:flutter_styled_toast/flutter_styled_toast.dart'; import 'package:http/http.dart' as http; import 'package:http/http.dart'; import 'package:invoiceninja_flutter/main_app.dart'; import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart'; import 'package:invoiceninja_flutter/utils/files.dart'; import 'package:printing/printing.dart'; -import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher.dart'; // Project imports: @@ -242,33 +239,8 @@ class _InvoicePdfViewState extends State { WebUtils.downloadBinaryFile( fileName, _response!.bodyBytes); } else { - final directory = - await getAppDownloadDirectory(); - - if (directory == null) { - return; - } - - String filePath = - '$directory${file.Platform.pathSeparator}$fileName'; - - if (file.File(filePath).existsSync()) { - final timestamp = - DateTime.now().millisecondsSinceEpoch; - filePath = filePath.replaceFirst( - '.pdf', '_$timestamp.pdf'); - } - - final pdfData = file.File(filePath); - await pdfData - .writeAsBytes(_response!.bodyBytes); - - if (isDesktopOS()) { - showToast(localization.fileSavedInPath - .replaceFirst(':path', directory)); - } else { - await Share.shareXFiles([XFile(filePath)]); - } + saveDownloadedFile( + _response!.bodyBytes, fileName); } } }, diff --git a/lib/utils/files.dart b/lib/utils/files.dart index 98057df24..dc0f29418 100644 --- a/lib/utils/files.dart +++ b/lib/utils/files.dart @@ -1,5 +1,6 @@ // Dart imports: import 'dart:io'; +import 'dart:io' as file; // Flutter imports: import 'package:flutter/foundation.dart'; @@ -7,6 +8,7 @@ import 'package:flutter/foundation.dart'; // Package imports: import 'package:file_picker/file_picker.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:flutter_styled_toast/flutter_styled_toast.dart'; import 'package:http/http.dart'; import 'package:invoiceninja_flutter/main_app.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -21,6 +23,7 @@ import 'package:invoiceninja_flutter/utils/platforms.dart'; // ignore: unused_import import 'package:invoiceninja_flutter/utils/web_stub.dart' if (dart.library.html) 'package:invoiceninja_flutter/utils/web.dart'; +import 'package:share_plus/share_plus.dart'; Future?> pickFiles({ String? fileIndex, @@ -84,6 +87,29 @@ Future?> _pickFiles({ return null; } +void saveDownloadedFile(Uint8List data, String fileName) async { + final directory = await getAppDownloadDirectory(); + if (directory != null) { + String filePath = '$directory/${file.Platform.pathSeparator}$fileName'; + + if (file.File(filePath).existsSync()) { + final extension = fileName.split('.').last; + final timestamp = DateTime.now().millisecondsSinceEpoch; + filePath = filePath.replaceFirst('.$extension', '_$timestamp.$extension'); + } + + await File(filePath).writeAsBytes(data); + + if (isDesktopOS()) { + showToast(AppLocalization.of(navigatorKey.currentContext!)! + .fileSavedInPath + .replaceFirst(':path', directory)); + } else { + await Share.shareXFiles([XFile(filePath)]); + } + } +} + Future getAppDownloadDirectory() async { var path = ''; From 9c75bab043c2cc82420608d559f77a678e900098 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 8 Nov 2023 13:41:58 +0200 Subject: [PATCH 097/138] Send token for PDF download --- lib/data/models/invoice_model.dart | 3 --- lib/data/models/invoice_model.g.dart | 23 ------------------ lib/redux/credit/credit_actions.dart | 13 +++++++++- lib/redux/invoice/invoice_actions.dart | 24 +++++++++++++++++-- .../purchase_order_actions.dart | 13 +++++++++- lib/redux/quote/quote_actions.dart | 12 +++++++++- 6 files changed, 57 insertions(+), 31 deletions(-) diff --git a/lib/data/models/invoice_model.dart b/lib/data/models/invoice_model.dart index 8c5487de3..ef3192008 100644 --- a/lib/data/models/invoice_model.dart +++ b/lib/data/models/invoice_model.dart @@ -214,7 +214,6 @@ abstract class InvoiceEntity extends Object customSurcharge2: 0, customSurcharge3: 0, customSurcharge4: 0, - filename: '', subscriptionId: '', recurringDates: BuiltList(), lineItems: BuiltList(), @@ -554,8 +553,6 @@ abstract class InvoiceEntity extends Object @BuiltValueField(wireName: 'auto_bill_enabled') bool get autoBillEnabled; - String? get filename; - @BuiltValueField(wireName: 'recurring_dates') BuiltList? get recurringDates; diff --git a/lib/data/models/invoice_model.g.dart b/lib/data/models/invoice_model.g.dart index 8e643f054..f4f693205 100644 --- a/lib/data/models/invoice_model.g.dart +++ b/lib/data/models/invoice_model.g.dart @@ -374,13 +374,6 @@ class _$InvoiceEntitySerializer implements StructuredSerializer { ..add(serializers.serialize(value, specifiedType: const FullType(String))); } - value = object.filename; - if (value != null) { - result - ..add('filename') - ..add(serializers.serialize(value, - specifiedType: const FullType(String))); - } value = object.recurringDates; if (value != null) { result @@ -677,10 +670,6 @@ class _$InvoiceEntitySerializer implements StructuredSerializer { result.autoBillEnabled = serializers.deserialize(value, specifiedType: const FullType(bool))! as bool; break; - case 'filename': - result.filename = serializers.deserialize(value, - specifiedType: const FullType(String)) as String?; - break; case 'recurring_dates': result.recurringDates.replace(serializers.deserialize(value, specifiedType: const FullType(BuiltList, const [ @@ -1570,8 +1559,6 @@ class _$InvoiceEntity extends InvoiceEntity { @override final bool autoBillEnabled; @override - final String? filename; - @override final BuiltList? recurringDates; @override final BuiltList lineItems; @@ -1670,7 +1657,6 @@ class _$InvoiceEntity extends InvoiceEntity { this.invoiceId, this.recurringId, required this.autoBillEnabled, - this.filename, this.recurringDates, required this.lineItems, required this.invitations, @@ -1867,7 +1853,6 @@ class _$InvoiceEntity extends InvoiceEntity { invoiceId == other.invoiceId && recurringId == other.recurringId && autoBillEnabled == other.autoBillEnabled && - filename == other.filename && recurringDates == other.recurringDates && lineItems == other.lineItems && invitations == other.invitations && @@ -1949,7 +1934,6 @@ class _$InvoiceEntity extends InvoiceEntity { _$hash = $jc(_$hash, invoiceId.hashCode); _$hash = $jc(_$hash, recurringId.hashCode); _$hash = $jc(_$hash, autoBillEnabled.hashCode); - _$hash = $jc(_$hash, filename.hashCode); _$hash = $jc(_$hash, recurringDates.hashCode); _$hash = $jc(_$hash, lineItems.hashCode); _$hash = $jc(_$hash, invitations.hashCode); @@ -2031,7 +2015,6 @@ class _$InvoiceEntity extends InvoiceEntity { ..add('invoiceId', invoiceId) ..add('recurringId', recurringId) ..add('autoBillEnabled', autoBillEnabled) - ..add('filename', filename) ..add('recurringDates', recurringDates) ..add('lineItems', lineItems) ..add('invitations', invitations) @@ -2306,10 +2289,6 @@ class InvoiceEntityBuilder set autoBillEnabled(bool? autoBillEnabled) => _$this._autoBillEnabled = autoBillEnabled; - String? _filename; - String? get filename => _$this._filename; - set filename(String? filename) => _$this._filename = filename; - ListBuilder? _recurringDates; ListBuilder get recurringDates => _$this._recurringDates ??= new ListBuilder(); @@ -2462,7 +2441,6 @@ class InvoiceEntityBuilder _invoiceId = $v.invoiceId; _recurringId = $v.recurringId; _autoBillEnabled = $v.autoBillEnabled; - _filename = $v.filename; _recurringDates = $v.recurringDates?.toBuilder(); _lineItems = $v.lineItems.toBuilder(); _invitations = $v.invitations.toBuilder(); @@ -2571,7 +2549,6 @@ class InvoiceEntityBuilder invoiceId: invoiceId, recurringId: recurringId, autoBillEnabled: BuiltValueNullFieldError.checkNotNull(autoBillEnabled, r'InvoiceEntity', 'autoBillEnabled'), - filename: filename, recurringDates: _recurringDates?.build(), lineItems: lineItems.build(), invitations: invitations.build(), diff --git a/lib/redux/credit/credit_actions.dart b/lib/redux/credit/credit_actions.dart index b0d663638..62b0550b4 100644 --- a/lib/redux/credit/credit_actions.dart +++ b/lib/redux/credit/credit_actions.dart @@ -11,6 +11,7 @@ import 'package:flutter_redux/flutter_redux.dart'; import 'package:http/http.dart'; import 'package:invoiceninja_flutter/redux/document/document_actions.dart'; import 'package:invoiceninja_flutter/redux/settings/settings_actions.dart'; +import 'package:invoiceninja_flutter/utils/files.dart'; import 'package:url_launcher/url_launcher.dart'; // Project imports: @@ -628,7 +629,17 @@ Future handleCreditAction(BuildContext context, List credits, ); break; case EntityAction.download: - launchUrl(Uri.parse(credit.invitationDownloadLink)); + store.dispatch(StartLoading()); + await WebClient() + .get(credit.invitationEInvoiceDownloadLink, state.token, + rawResponse: true) + .then((response) { + store.dispatch(StopLoading()); + saveDownloadedFile(response.bodyBytes, + localization!.credit + '_' + credit.number + '.pdf'); + }).catchError((_) { + store.dispatch(StopLoading()); + }); break; case EntityAction.bulkDownload: store.dispatch(DownloadCreditsRequest( diff --git a/lib/redux/invoice/invoice_actions.dart b/lib/redux/invoice/invoice_actions.dart index 08c554573..6f0f2b969 100644 --- a/lib/redux/invoice/invoice_actions.dart +++ b/lib/redux/invoice/invoice_actions.dart @@ -13,6 +13,7 @@ import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/redux/client/client_selectors.dart'; import 'package:invoiceninja_flutter/redux/document/document_actions.dart'; import 'package:invoiceninja_flutter/redux/settings/settings_actions.dart'; +import 'package:invoiceninja_flutter/utils/files.dart'; import 'package:url_launcher/url_launcher.dart'; // Project imports: @@ -771,10 +772,29 @@ void handleInvoiceAction(BuildContext? context, List invoices, ); break; case EntityAction.download: - launchUrl(Uri.parse(invoice.invitationDownloadLink)); + store.dispatch(StartLoading()); + await WebClient() + .get(invoice.invitationDownloadLink, state.token, rawResponse: true) + .then((response) { + store.dispatch(StopLoading()); + saveDownloadedFile(response.bodyBytes, + localization!.invoice + '_' + invoice.number + '.pdf'); + }).catchError((_) { + store.dispatch(StopLoading()); + }); break; case EntityAction.eInvoice: - launchUrl(Uri.parse(invoice.invitationEInvoiceDownloadLink)); + store.dispatch(StartLoading()); + await WebClient() + .get(invoice.invitationEInvoiceDownloadLink, state.token, + rawResponse: true) + .then((response) { + store.dispatch(StopLoading()); + saveDownloadedFile(response.bodyBytes, + localization!.invoice + '_' + invoice.number + '.xml'); + }).catchError((_) { + store.dispatch(StopLoading()); + }); break; case EntityAction.bulkDownload: store.dispatch(DownloadInvoicesRequest( diff --git a/lib/redux/purchase_order/purchase_order_actions.dart b/lib/redux/purchase_order/purchase_order_actions.dart index 6574ee612..a5153c757 100644 --- a/lib/redux/purchase_order/purchase_order_actions.dart +++ b/lib/redux/purchase_order/purchase_order_actions.dart @@ -15,6 +15,7 @@ import 'package:invoiceninja_flutter/redux/design/design_selectors.dart'; import 'package:invoiceninja_flutter/redux/settings/settings_actions.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/localization.dart'; import 'package:invoiceninja_flutter/ui/app/entities/entity_actions_dialog.dart'; import 'package:printing/printing.dart'; @@ -827,7 +828,17 @@ void handlePurchaseOrderAction(BuildContext? context, .recreateInvitations(state)); break; case EntityAction.download: - launchUrl(Uri.parse(purchaseOrder.invitationDownloadLink)); + await WebClient() + .get(purchaseOrder.invitationEInvoiceDownloadLink, state.token, + rawResponse: true) + .then((response) { + store.dispatch(StopLoading()); + saveDownloadedFile(response.bodyBytes, + localization!.purchaseOrder + '_' + purchaseOrder.number + '.pdf'); + }).catchError((_) { + store.dispatch(StopLoading()); + }); + break; case EntityAction.bulkDownload: store.dispatch(DownloadPurchaseOrdersRequest( diff --git a/lib/redux/quote/quote_actions.dart b/lib/redux/quote/quote_actions.dart index 1def75eea..27c814ed1 100644 --- a/lib/redux/quote/quote_actions.dart +++ b/lib/redux/quote/quote_actions.dart @@ -12,6 +12,7 @@ import 'package:http/http.dart'; import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/redux/document/document_actions.dart'; import 'package:invoiceninja_flutter/redux/settings/settings_actions.dart'; +import 'package:invoiceninja_flutter/utils/files.dart'; import 'package:url_launcher/url_launcher.dart'; // Project imports: @@ -681,7 +682,16 @@ Future handleQuoteAction( ..designId = designId)); break; case EntityAction.download: - launchUrl(Uri.parse(quote.invitationDownloadLink)); + store.dispatch(StartLoading()); + await WebClient() + .get(quote.invitationDownloadLink, state.token, rawResponse: true) + .then((response) { + store.dispatch(StopLoading()); + saveDownloadedFile(response.bodyBytes, + localization!.quote + '_' + quote.number + '.pdf'); + }).catchError((_) { + store.dispatch(StopLoading()); + }); break; case EntityAction.bulkDownload: store.dispatch(DownloadQuotesRequest( From 135851cf04469cdd728934ef7d27918031dd2d4b Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 8 Nov 2023 16:08:26 +0200 Subject: [PATCH 098/138] Send token for PDF download --- lib/redux/document/document_actions.dart | 12 +------- lib/ui/client/client_pdf.dart | 11 +------- lib/ui/invoice/invoice_pdf.dart | 28 ++++++------------- lib/utils/files.dart | 35 ++++++++++++++---------- 4 files changed, 30 insertions(+), 56 deletions(-) diff --git a/lib/redux/document/document_actions.dart b/lib/redux/document/document_actions.dart index 44ec4bc3d..9b81551f8 100644 --- a/lib/redux/document/document_actions.dart +++ b/lib/redux/document/document_actions.dart @@ -3,7 +3,6 @@ import 'dart:async'; // Flutter imports: import 'package:built_collection/built_collection.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; // Package imports: @@ -36,9 +35,6 @@ import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:pinch_zoom/pinch_zoom.dart'; import 'package:printing/printing.dart'; -import 'package:invoiceninja_flutter/utils/web_stub.dart' - if (dart.library.html) 'package:invoiceninja_flutter/utils/web.dart'; - class ViewDocumentList implements PersistUI { ViewDocumentList({ this.force = false, @@ -430,13 +426,7 @@ void handleDocumentAction( void downloadDocument() async { final DocumentEntity? document = store.state.documentState.map[documentIds.first]; - if (kIsWeb) { - if (document?.data != null) { - WebUtils.downloadBinaryFile(document!.name, document.data!); - } - } else { - saveDownloadedFile(document!.data!, document.name); - } + saveDownloadedFile(document!.data!, document.name); } if (document.data == null) { store.dispatch(LoadDocumentData( diff --git a/lib/ui/client/client_pdf.dart b/lib/ui/client/client_pdf.dart index 48d20c734..d7f9d3954 100644 --- a/lib/ui/client/client_pdf.dart +++ b/lib/ui/client/client_pdf.dart @@ -4,7 +4,6 @@ import 'dart:convert'; // Flutter imports: import 'package:built_collection/built_collection.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; // Package imports: @@ -38,9 +37,6 @@ import 'package:invoiceninja_flutter/utils/dialogs.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart'; -import 'package:invoiceninja_flutter/utils/web_stub.dart' - if (dart.library.html) 'package:invoiceninja_flutter/utils/web.dart'; - class ClientPdfView extends StatefulWidget { const ClientPdfView({ Key? key, @@ -333,12 +329,7 @@ class _ClientPdfViewState extends State { '_' + (client!.number) + '.pdf'; - if (kIsWeb) { - WebUtils.downloadBinaryFile( - fileName, _response!.bodyBytes); - } else { - saveDownloadedFile(_response!.bodyBytes, fileName); - } + saveDownloadedFile(_response!.bodyBytes, fileName); }, ), AppTextButton( diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index 0b302c4de..53acd5322 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -14,7 +14,6 @@ import 'package:invoiceninja_flutter/main_app.dart'; import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart'; import 'package:invoiceninja_flutter/utils/files.dart'; import 'package:printing/printing.dart'; -import 'package:url_launcher/url_launcher.dart'; // Project imports: import 'package:invoiceninja_flutter/data/models/models.dart'; @@ -224,25 +223,14 @@ class _InvoicePdfViewState extends State { onPressed: _response == null ? null : () async { - if (_response == null) { - launchUrl( - Uri.parse(invoice.invitationDownloadLink)); - } else { - final fileName = - localization.lookup('${invoice.entityType}') + - '_' + - (invoice.number.isEmpty - ? localization.pending - : invoice.number) + - '.pdf'; - if (kIsWeb) { - WebUtils.downloadBinaryFile( - fileName, _response!.bodyBytes); - } else { - saveDownloadedFile( - _response!.bodyBytes, fileName); - } - } + final fileName = + localization.lookup('${invoice.entityType}') + + '_' + + (invoice.number.isEmpty + ? localization.pending + : invoice.number) + + '.pdf'; + saveDownloadedFile(_response!.bodyBytes, fileName); }, ), if (isDesktop(context)) diff --git a/lib/utils/files.dart b/lib/utils/files.dart index dc0f29418..8ed69a243 100644 --- a/lib/utils/files.dart +++ b/lib/utils/files.dart @@ -88,24 +88,29 @@ Future?> _pickFiles({ } void saveDownloadedFile(Uint8List data, String fileName) async { - final directory = await getAppDownloadDirectory(); - if (directory != null) { - String filePath = '$directory/${file.Platform.pathSeparator}$fileName'; + if (kIsWeb) { + WebUtils.downloadBinaryFile(fileName, data); + } else { + final directory = await getAppDownloadDirectory(); + if (directory != null) { + String filePath = '$directory/${file.Platform.pathSeparator}$fileName'; - if (file.File(filePath).existsSync()) { - final extension = fileName.split('.').last; - final timestamp = DateTime.now().millisecondsSinceEpoch; - filePath = filePath.replaceFirst('.$extension', '_$timestamp.$extension'); - } + if (file.File(filePath).existsSync()) { + final extension = fileName.split('.').last; + final timestamp = DateTime.now().millisecondsSinceEpoch; + filePath = + filePath.replaceFirst('.$extension', '_$timestamp.$extension'); + } - await File(filePath).writeAsBytes(data); + await File(filePath).writeAsBytes(data); - if (isDesktopOS()) { - showToast(AppLocalization.of(navigatorKey.currentContext!)! - .fileSavedInPath - .replaceFirst(':path', directory)); - } else { - await Share.shareXFiles([XFile(filePath)]); + if (isDesktopOS()) { + showToast(AppLocalization.of(navigatorKey.currentContext!)! + .fileSavedInPath + .replaceFirst(':path', directory)); + } else { + await Share.shareXFiles([XFile(filePath)]); + } } } } From 8dfe0e38d100cf912349d7911e3d7591b24f45b4 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 8 Nov 2023 16:13:01 +0200 Subject: [PATCH 099/138] Adjust maxPageWidth for PDFs --- lib/ui/client/client_pdf.dart | 2 +- lib/ui/design/edit/design_edit.dart | 2 +- lib/ui/invoice/edit/invoice_edit_desktop.dart | 2 +- lib/ui/invoice/edit/invoice_edit_pdf.dart | 2 +- lib/ui/invoice/invoice_pdf.dart | 2 +- lib/ui/settings/invoice_design.dart | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/ui/client/client_pdf.dart b/lib/ui/client/client_pdf.dart index d7f9d3954..6b3dfd27f 100644 --- a/lib/ui/client/client_pdf.dart +++ b/lib/ui/client/client_pdf.dart @@ -462,7 +462,7 @@ class _ClientPdfViewState extends State { canChangeOrientation: false, canChangePageFormat: false, canDebug: false, - maxPageWidth: 800, + maxPageWidth: 600, pdfFileName: localization!.statement + '_' + client!.number + '.pdf', ), diff --git a/lib/ui/design/edit/design_edit.dart b/lib/ui/design/edit/design_edit.dart index afe447a6c..85a78223a 100644 --- a/lib/ui/design/edit/design_edit.dart +++ b/lib/ui/design/edit/design_edit.dart @@ -689,7 +689,7 @@ class _PdfDesignPreviewState extends State { allowPrinting: false, allowSharing: false, canDebug: false, - maxPageWidth: 800, + maxPageWidth: 600, ) else SizedBox(), diff --git a/lib/ui/invoice/edit/invoice_edit_desktop.dart b/lib/ui/invoice/edit/invoice_edit_desktop.dart index ee6e659de..6ac976ffe 100644 --- a/lib/ui/invoice/edit/invoice_edit_desktop.dart +++ b/lib/ui/invoice/edit/invoice_edit_desktop.dart @@ -1352,7 +1352,7 @@ class __PdfPreviewState extends State<_PdfPreview> { allowSharing: false, canDebug: false, pages: [_currentPage - 1], - maxPageWidth: 800, + maxPageWidth: 600, ), ), ], diff --git a/lib/ui/invoice/edit/invoice_edit_pdf.dart b/lib/ui/invoice/edit/invoice_edit_pdf.dart index c029e72f3..02e7bc0ea 100644 --- a/lib/ui/invoice/edit/invoice_edit_pdf.dart +++ b/lib/ui/invoice/edit/invoice_edit_pdf.dart @@ -115,7 +115,7 @@ class InvoiceEditPDFState extends State { allowPrinting: false, allowSharing: false, canDebug: false, - maxPageWidth: 800, + maxPageWidth: 600, ), ); } diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index 53acd5322..88ff1ddeb 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -263,7 +263,7 @@ class _InvoicePdfViewState extends State { canChangeOrientation: false, canChangePageFormat: false, canDebug: false, - maxPageWidth: 800, + maxPageWidth: 600, pdfFileName: localization.lookup(invoice.entityType!.snakeCase) + '_' + diff --git a/lib/ui/settings/invoice_design.dart b/lib/ui/settings/invoice_design.dart index 6e9397da6..84491515d 100644 --- a/lib/ui/settings/invoice_design.dart +++ b/lib/ui/settings/invoice_design.dart @@ -1380,7 +1380,7 @@ class _PdfPreviewState extends State<_PdfPreview> { canChangeOrientation: false, canChangePageFormat: false, canDebug: false, - maxPageWidth: 800, + maxPageWidth: 600, allowPrinting: false, allowSharing: false, ), From 5bb5d4ce57b0c7a4967251411f377398e1e4f8eb Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 8 Nov 2023 16:52:27 +0200 Subject: [PATCH 100/138] Adjust style of app dropdown button --- lib/ui/app/forms/app_dropdown_button.dart | 35 ++++++++++++----------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/lib/ui/app/forms/app_dropdown_button.dart b/lib/ui/app/forms/app_dropdown_button.dart index ebae2f6f3..22c79cac5 100644 --- a/lib/ui/app/forms/app_dropdown_button.dart +++ b/lib/ui/app/forms/app_dropdown_button.dart @@ -34,25 +34,26 @@ class AppDropdownButton extends StatelessWidget { } final bool isEmpty = checkedValue == null || checkedValue == ''; - Widget dropDownButton = DropdownButtonHideUnderline( - child: DropdownButton( - value: checkedValue, - isExpanded: true, - isDense: labelText != null, - onChanged: enabled ? onChanged : null, - selectedItemBuilder: selectedItemBuilder, - items: [ - if (showBlank || isEmpty) - DropdownMenuItem( - value: blankValue, - child: blankLabel == null ? SizedBox() : Text(blankLabel!), - ), - ...items - ], - ), + Widget dropDownButton = DropdownButtonFormField( + decoration: labelText != null + ? InputDecoration(label: Text(labelText!)) + : InputDecoration.collapsed(hintText: ''), + value: checkedValue == blankValue ? null : checkedValue, + isExpanded: true, + isDense: labelText != null, + onChanged: enabled ? onChanged : null, + selectedItemBuilder: selectedItemBuilder, + items: [ + if (showBlank || isEmpty) + DropdownMenuItem( + value: blankValue, + child: blankLabel == null ? SizedBox() : Text(blankLabel!), + ), + ...items + ], ); - if (labelText != null) { + if (false && labelText != null) { dropDownButton = InputDecorator( decoration: InputDecoration( labelText: labelText!.isEmpty ? null : labelText, From e51d6a4963393333b269529ca276669e6ac2ad3c Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 8 Nov 2023 16:52:48 +0200 Subject: [PATCH 101/138] Add designs templates --- lib/ui/invoice/invoice_pdf.dart | 95 +++++++++++++++++++++------------ 1 file changed, 62 insertions(+), 33 deletions(-) diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index 88ff1ddeb..85918dc3e 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -12,6 +12,7 @@ import 'package:http/http.dart' as http; import 'package:http/http.dart'; import 'package:invoiceninja_flutter/main_app.dart'; import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart'; +import 'package:invoiceninja_flutter/ui/app/forms/design_picker.dart'; import 'package:invoiceninja_flutter/utils/files.dart'; import 'package:printing/printing.dart'; @@ -49,6 +50,7 @@ class InvoicePdfView extends StatefulWidget { class _InvoicePdfViewState extends State { bool _isLoading = true; bool _isDeliveryNote = false; + String? _designId; String? _activityId; String? _pdfString; http.Response? _response; @@ -139,38 +141,64 @@ class _InvoicePdfViewState extends State { ]; */ - final activitySelector = _activityId == null || kIsWeb - ? SizedBox() - : Padding( - padding: const EdgeInsets.only(left: 17), - child: SizedBox( - width: 350, - child: IgnorePointer( - ignoring: _isLoading, - child: AppDropdownButton( - value: _activityId, - onChanged: (dynamic activityId) { - setState(() { - _activityId = activityId; - loadPdf(); - }); - }, - items: invoice.balanceHistory - .map((history) => DropdownMenuItem( - child: Text(formatNumber(history.amount, context, - clientId: invoice.clientId)! + - ' • ' + - formatDate( - convertTimestampToDateString( - history.createdAt), - context, - showTime: true)), - value: history.activityId, - )) - .toList()), - ), - ), - ); + final activitySelector = + _activityId == null || (kIsWeb && state.prefState.enableNativeBrowser) + ? SizedBox() + : Padding( + padding: const EdgeInsets.only(left: 17), + child: SizedBox( + width: 350, + child: IgnorePointer( + ignoring: _isLoading, + child: AppDropdownButton( + value: _activityId, + onChanged: (dynamic activityId) { + setState(() { + _activityId = activityId; + loadPdf(); + }); + }, + items: invoice.balanceHistory + .map((history) => DropdownMenuItem( + child: Text(formatNumber( + history.amount, context, + clientId: invoice.clientId)! + + ' • ' + + formatDate( + convertTimestampToDateString( + history.createdAt), + context, + showTime: true)), + value: history.activityId, + )) + .toList()), + ), + ), + ); + + final designSelector = + _activityId != null || (kIsWeb && state.prefState.enableNativeBrowser) + ? SizedBox() + : Padding( + padding: const EdgeInsets.only(left: 17), + child: SizedBox( + width: 350, + child: IgnorePointer( + ignoring: _isLoading, + child: DesignPicker( + initialValue: _designId, + onSelected: (design) { + setState(() { + _designId = design?.id; + loadPdf(); + }); + }, + label: localization.design, + showBlank: true, + ), + ), + ), + ); final deliveryNote = Container( width: 200, @@ -248,8 +276,9 @@ class _InvoicePdfViewState extends State { Material( child: Row( children: [ - if (invoice.isInvoice && _activityId == null) deliveryNote, + designSelector, activitySelector, + if (invoice.isInvoice && _activityId == null) deliveryNote, ], ), ), From daa29531dd47e2ebb6045659a761996172c2c4bb Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 8 Nov 2023 16:53:20 +0200 Subject: [PATCH 102/138] Adjust style of app dropdown button --- lib/ui/app/forms/app_dropdown_button.dart | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/lib/ui/app/forms/app_dropdown_button.dart b/lib/ui/app/forms/app_dropdown_button.dart index 22c79cac5..ebb0d7623 100644 --- a/lib/ui/app/forms/app_dropdown_button.dart +++ b/lib/ui/app/forms/app_dropdown_button.dart @@ -34,7 +34,7 @@ class AppDropdownButton extends StatelessWidget { } final bool isEmpty = checkedValue == null || checkedValue == ''; - Widget dropDownButton = DropdownButtonFormField( + return DropdownButtonFormField( decoration: labelText != null ? InputDecoration(label: Text(labelText!)) : InputDecoration.collapsed(hintText: ''), @@ -52,16 +52,5 @@ class AppDropdownButton extends StatelessWidget { ...items ], ); - - if (false && labelText != null) { - dropDownButton = InputDecorator( - decoration: InputDecoration( - labelText: labelText!.isEmpty ? null : labelText, - ), - isEmpty: isEmpty && blankLabel == null, - child: dropDownButton); - } - - return dropDownButton; } } From 09a0d26b883d558e1088793d5dd513c846106e2b Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 8 Nov 2023 17:02:30 +0200 Subject: [PATCH 103/138] Add designs templates --- lib/ui/invoice/invoice_pdf.dart | 4 ++-- lib/ui/settings/settings_list.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index 85918dc3e..44b8ca98e 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -182,7 +182,7 @@ class _InvoicePdfViewState extends State { : Padding( padding: const EdgeInsets.only(left: 17), child: SizedBox( - width: 350, + width: 200, child: IgnorePointer( ignoring: _isLoading, child: DesignPicker( @@ -276,7 +276,7 @@ class _InvoicePdfViewState extends State { Material( child: Row( children: [ - designSelector, + if (supportsDesignTemplates()) designSelector, activitySelector, if (invoice.isInvoice && _activityId == null) deliveryNote, ], diff --git a/lib/ui/settings/settings_list.dart b/lib/ui/settings/settings_list.dart index bf55e24d8..7ac728f1b 100644 --- a/lib/ui/settings/settings_list.dart +++ b/lib/ui/settings/settings_list.dart @@ -555,7 +555,7 @@ class SettingsSearch extends StatelessWidget { 'delivery_note_design#2023-11-06', 'statement_design#2023-11-06', 'payment_receipt_design#2023-11-06', - 'payment_refund_design#2023-11-06', + //'payment_refund_design#2023-11-06', ], ], ], From 5d0a882b801c9da97363de64462c8e598664a716 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 9 Nov 2023 11:06:25 +0200 Subject: [PATCH 104/138] Add designs templates --- lib/ui/design/edit/design_edit.dart | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/ui/design/edit/design_edit.dart b/lib/ui/design/edit/design_edit.dart index 85a78223a..e80c1dba9 100644 --- a/lib/ui/design/edit/design_edit.dart +++ b/lib/ui/design/edit/design_edit.dart @@ -448,6 +448,7 @@ class _DesignSettingsState extends State { @override Widget build(BuildContext context) { final localization = AppLocalization.of(context)!; + final state = widget.viewModel.state; final design = widget.viewModel.design; final entityTypes = design.entities.split(','); @@ -464,13 +465,16 @@ class _DesignSettingsState extends State { value.isEmpty ? localization.pleaseEnterAName : null, ), DesignPicker( - label: localization.design, - onSelected: (value) { + showBlank: true, + label: localization.loadDesign, + onSelected: (value) { + if (value != null) { widget.onLoadDesign(value!); _selectedDesign = value; - }, - initialValue: _selectedDesign?.id), - SizedBox(height: 16), + } + }, + ), + SizedBox(height: 20), // TODO remove this once browser supported on all platforms if (!kReleaseMode || kIsWeb || isMobileOS()) SwitchListTile( @@ -497,7 +501,15 @@ class _DesignSettingsState extends State { ...[ EntityType.client, EntityType.invoice, + EntityType.payment, + EntityType.quote, + EntityType.credit, + EntityType.project, + EntityType.task, + EntityType.purchaseOrder, ] + .where( + (entityType) => state.company.isModuleEnabled(entityType)) .map((entityType) => CheckboxListTile( value: entityTypes.contains(entityType.apiValue), onChanged: (value) { @@ -507,8 +519,10 @@ class _DesignSettingsState extends State { } else { entities.remove(entityType.apiValue); } - widget.viewModel.onChanged(design.rebuild( - (b) => b..entities = entities.join(','))); + widget.viewModel.onChanged(design.rebuild((b) => b + ..entities = entities + .where((entity) => entity.isNotEmpty) + .join(','))); }, title: Text(localization.lookup(entityType.plural)), controlAffinity: ListTileControlAffinity.leading, From 78312801ebf0e8652be38775d9f5788981832288 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 9 Nov 2023 11:07:39 +0200 Subject: [PATCH 105/138] Add designs templates --- lib/ui/design/edit/design_edit.dart | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/lib/ui/design/edit/design_edit.dart b/lib/ui/design/edit/design_edit.dart index e80c1dba9..dfd8900f0 100644 --- a/lib/ui/design/edit/design_edit.dart +++ b/lib/ui/design/edit/design_edit.dart @@ -426,25 +426,6 @@ class DesignSettings extends StatefulWidget { } class _DesignSettingsState extends State { - DesignEntity? _selectedDesign; - - @override - void initState() { - super.initState(); - - final viewModel = widget.viewModel; - final design = viewModel.design; - - if (design.isOld) { - _selectedDesign = design; - } else { - final state = viewModel.state; - final designMap = state.designState.map; - _selectedDesign = - designMap[state.company.settings.defaultInvoiceDesignId]; - } - } - @override Widget build(BuildContext context) { final localization = AppLocalization.of(context)!; @@ -469,8 +450,7 @@ class _DesignSettingsState extends State { label: localization.loadDesign, onSelected: (value) { if (value != null) { - widget.onLoadDesign(value!); - _selectedDesign = value; + widget.onLoadDesign(value); } }, ), From 2be0ed20894c01b3743d96cfcf5fd8750af926da Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 9 Nov 2023 12:45:25 +0200 Subject: [PATCH 106/138] Add designs templates --- lib/data/models/design_model.dart | 3 +++ lib/ui/app/forms/design_picker.dart | 12 +++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/data/models/design_model.dart b/lib/data/models/design_model.dart index aa3e4fa9d..d6d8607cc 100644 --- a/lib/data/models/design_model.dart +++ b/lib/data/models/design_model.dart @@ -231,6 +231,9 @@ abstract class DesignEntity extends Object @override FormatNumberType? get listDisplayAmountType => null; + bool supportsEntityType(EntityType entityType) => + isTemplate && entities.split(',').contains(entityType.apiValue); + // ignore: unused_element static void _initializeBuilder(DesignEntityBuilder builder) => builder ..isFree = true diff --git a/lib/ui/app/forms/design_picker.dart b/lib/ui/app/forms/design_picker.dart index 115e2d7d4..2cef4032c 100644 --- a/lib/ui/app/forms/design_picker.dart +++ b/lib/ui/app/forms/design_picker.dart @@ -6,6 +6,7 @@ import 'package:flutter_redux/flutter_redux.dart'; // Project imports: import 'package:invoiceninja_flutter/data/models/design_model.dart'; +import 'package:invoiceninja_flutter/data/models/entities.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/ui/app/forms/app_dropdown_button.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; @@ -16,12 +17,14 @@ class DesignPicker extends StatelessWidget { this.label, this.initialValue, this.showBlank = false, + this.entityType, }); final Function(DesignEntity?) onSelected; final String? label; final String? initialValue; final bool showBlank; + final EntityType? entityType; @override Widget build(BuildContext context) { @@ -36,14 +39,17 @@ class DesignPicker extends StatelessWidget { onChanged: (dynamic value) => onSelected(designState.map[value]), items: designState.list .where((designId) { - final design = designState.map[designId]; + final design = designState.map[designId]!; if (state.isHosted && !state.isPaidAccount && !state.account.isTrial && - !design!.isFree) { + !design.isFree) { return false; } - return design!.isActive || designId == initialValue; + if (entityType != null && !design.supportsEntityType(entityType!)) { + return false; + } + return design.isActive || designId == initialValue; }) .map((value) => DropdownMenuItem( value: value, From b83b2e436128e30868e3d6a45d8c693c17bc2ad1 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 9 Nov 2023 13:07:20 +0200 Subject: [PATCH 107/138] Add designs templates --- lib/redux/design/design_selectors.dart | 13 +++++++ lib/ui/invoice/invoice_pdf.dart | 50 ++++++++++++++------------ 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/lib/redux/design/design_selectors.dart b/lib/redux/design/design_selectors.dart index 976235648..8ed988260 100644 --- a/lib/redux/design/design_selectors.dart +++ b/lib/redux/design/design_selectors.dart @@ -98,3 +98,16 @@ String? getDesignIdForVendorByEntity( return settings.defaultInvoiceDesignId; } } + +bool hasDesignTemplatesForEntityType( + BuiltMap designMap, EntityType entityType) { + var hasMatch = false; + + designMap.forEach((designId, design) { + if (design.supportsEntityType(entityType)) { + hasMatch = true; + } + }); + + return hasMatch; +} diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index 44b8ca98e..4befed785 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -11,6 +11,7 @@ import 'package:flutter_redux/flutter_redux.dart'; import 'package:http/http.dart' as http; import 'package:http/http.dart'; import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/redux/design/design_selectors.dart'; import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart'; import 'package:invoiceninja_flutter/ui/app/forms/design_picker.dart'; import 'package:invoiceninja_flutter/utils/files.dart'; @@ -88,6 +89,7 @@ class _InvoicePdfViewState extends State { invoice, _isDeliveryNote, _activityId, + _designId, ).then((response) async { setState(() { _response = response; @@ -176,29 +178,32 @@ class _InvoicePdfViewState extends State { ), ); - final designSelector = - _activityId != null || (kIsWeb && state.prefState.enableNativeBrowser) - ? SizedBox() - : Padding( - padding: const EdgeInsets.only(left: 17), - child: SizedBox( - width: 200, - child: IgnorePointer( - ignoring: _isLoading, - child: DesignPicker( - initialValue: _designId, - onSelected: (design) { - setState(() { - _designId = design?.id; - loadPdf(); - }); - }, - label: localization.design, - showBlank: true, - ), - ), + final designSelector = _activityId != null || + (kIsWeb && state.prefState.enableNativeBrowser) || + !hasDesignTemplatesForEntityType( + state.designState.map, invoice.entityType!) + ? SizedBox() + : Padding( + padding: const EdgeInsets.only(left: 17), + child: SizedBox( + width: 200, + child: IgnorePointer( + ignoring: _isLoading, + child: DesignPicker( + initialValue: _designId, + onSelected: (design) { + setState(() { + _designId = design?.id; + loadPdf(); + }); + }, + label: localization.design, + showBlank: true, + entityType: invoice.entityType, ), - ); + ), + ), + ); final deliveryNote = Container( width: 200, @@ -310,6 +315,7 @@ Future _loadPDF( InvoiceEntity invoice, bool isDeliveryNote, String? activityId, + String? designId, ) async { http.Response? response; From 2490e21207b2a3bd436f28d6f688eabb3f6ec0c7 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 9 Nov 2023 14:59:08 +0200 Subject: [PATCH 108/138] Add designs templates --- lib/ui/invoice/invoice_pdf.dart | 76 ++++++++++++++++----------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index 4befed785..b3f4d2ef1 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -146,10 +146,9 @@ class _InvoicePdfViewState extends State { final activitySelector = _activityId == null || (kIsWeb && state.prefState.enableNativeBrowser) ? SizedBox() - : Padding( - padding: const EdgeInsets.only(left: 17), - child: SizedBox( - width: 350, + : Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 17), child: IgnorePointer( ignoring: _isLoading, child: AppDropdownButton( @@ -183,10 +182,9 @@ class _InvoicePdfViewState extends State { !hasDesignTemplatesForEntityType( state.designState.map, invoice.entityType!) ? SizedBox() - : Padding( - padding: const EdgeInsets.only(left: 17), - child: SizedBox( - width: 200, + : Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 17), child: IgnorePointer( ignoring: _isLoading, child: DesignPicker( @@ -205,8 +203,7 @@ class _InvoicePdfViewState extends State { ), ); - final deliveryNote = Container( - width: 200, + final deliveryNote = Flexible( child: CheckboxListTile( title: Text( localization.deliveryNote, @@ -277,35 +274,38 @@ class _InvoicePdfViewState extends State { ], ) : null, - body: Column(children: [ - Material( - child: Row( - children: [ - if (supportsDesignTemplates()) designSelector, - activitySelector, - if (invoice.isInvoice && _activityId == null) deliveryNote, - ], + body: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Material( + child: Row( + children: [ + if (supportsDesignTemplates()) designSelector, + activitySelector, + if (invoice.isInvoice && _activityId == null) deliveryNote, + ], + ), ), - ), - Expanded( - child: _isLoading || _response == null - ? LoadingIndicator() - : (kIsWeb && state.prefState.enableNativeBrowser) - ? HtmlElementView(viewType: _pdfString!) - : PdfPreview( - build: (format) => _response!.bodyBytes, - canChangeOrientation: false, - canChangePageFormat: false, - canDebug: false, - maxPageWidth: 600, - pdfFileName: - localization.lookup(invoice.entityType!.snakeCase) + - '_' + - invoice.number + - '.pdf', - ), - ), - ]), + Expanded( + child: _isLoading || _response == null + ? LoadingIndicator() + : (kIsWeb && state.prefState.enableNativeBrowser) + ? HtmlElementView(viewType: _pdfString!) + : PdfPreview( + build: (format) => _response!.bodyBytes, + canChangeOrientation: false, + canChangePageFormat: false, + canDebug: false, + maxPageWidth: 600, + pdfFileName: + localization.lookup(invoice.entityType!.snakeCase) + + '_' + + invoice.number + + '.pdf', + ), + ), + ], + ), ); } } From 4a3c5a1e0ba21d57e117a9765f0f20f1710b2773 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 9 Nov 2023 15:13:43 +0200 Subject: [PATCH 109/138] Add designs templates --- lib/ui/client/client_pdf.dart | 238 +++++++++++++++++--------------- lib/ui/invoice/invoice_pdf.dart | 1 - 2 files changed, 128 insertions(+), 111 deletions(-) diff --git a/lib/ui/client/client_pdf.dart b/lib/ui/client/client_pdf.dart index 6b3dfd27f..5b5ea3876 100644 --- a/lib/ui/client/client_pdf.dart +++ b/lib/ui/client/client_pdf.dart @@ -4,6 +4,7 @@ import 'dart:convert'; // Flutter imports: import 'package:built_collection/built_collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; // Package imports: @@ -19,6 +20,7 @@ import 'package:invoiceninja_flutter/ui/app/buttons/elevated_button.dart'; import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart'; import 'package:invoiceninja_flutter/ui/app/forms/date_picker.dart'; import 'package:invoiceninja_flutter/ui/app/multiselect.dart'; +import 'package:invoiceninja_flutter/ui/app/presenters/entity_presenter.dart'; import 'package:invoiceninja_flutter/utils/files.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:printing/printing.dart'; @@ -37,6 +39,9 @@ import 'package:invoiceninja_flutter/utils/dialogs.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart'; +import 'package:invoiceninja_flutter/utils/web_stub.dart' + if (dart.library.html) 'package:invoiceninja_flutter/utils/web.dart'; + class ClientPdfView extends StatefulWidget { const ClientPdfView({ Key? key, @@ -62,6 +67,7 @@ class _ClientPdfViewState extends State { convertDateTimeToSqlDate(DateTime.now().subtract(Duration(days: 365))); String? _endDate = convertDateTimeToSqlDate(); String _status = kStatementStatusAll; + String? _pdfString; @override void initState() { @@ -85,6 +91,7 @@ class _ClientPdfViewState extends State { } final localization = AppLocalization.of(context); + final state = widget.viewModel.state; setState(() { _isLoading = true; @@ -98,6 +105,12 @@ class _ClientPdfViewState extends State { } } else { _response = response; + + if (kIsWeb && state.prefState.enableNativeBrowser) { + _pdfString = 'data:application/pdf;base64,' + + base64Encode(response!.bodyBytes); + WebUtils.registerWebView(_pdfString); + } } _isLoading = false; @@ -172,8 +185,88 @@ class _ClientPdfViewState extends State { Widget build(BuildContext context) { final store = StoreProvider.of(context); final state = store.state; - final localization = AppLocalization.of(context); - final client = widget.viewModel.client; + final localization = AppLocalization.of(context)!; + final client = widget.viewModel.client!; + + final datePicker = Flexible( + child: Theme( + data: state.prefState.enableDarkMode || state.hasAccentColor + ? ThemeData.dark() + : ThemeData.light(), + child: AppDropdownButton( + labelText: localization.dateRange, + blankValue: null, + //showBlank: true, + value: _dateRange, + onChanged: (dynamic value) { + setState(() { + _dateRange = value; + }); + + if (value != DateRange.custom) { + loadPDF(); + } + }, + items: DateRange.values + .where((value) => value != DateRange.allTime) + .map((dateRange) => DropdownMenuItem( + child: Text(localization.lookup(dateRange.toString())), + value: dateRange, + )) + .toList(), + ), + ), + ); + + final statusPicker = Flexible( + child: Theme( + data: state.prefState.enableDarkMode || state.hasAccentColor + ? ThemeData.dark() + : ThemeData.light(), + child: AppDropdownButton( + labelText: localization.status, + blankValue: null, + value: _status, + onChanged: (dynamic value) { + setState(() { + _status = value; + }); + loadPDF(); + }, + items: [ + kStatementStatusAll, + kStatementStatusPaid, + kStatementStatusUnpaid, + ] + .map((value) => DropdownMenuItem( + child: Text(localization.lookup(value)), + value: value, + )) + .toList()), + ), + ); + + final sectionPicker = Flexible( + child: DropDownMultiSelect( + onChanged: (List selected) { + //_selectedOptions = selected; + store.dispatch(UpdateUserPreferences( + statementIncludes: BuiltList(selected))); + loadPDF(); + }, + selectedValues: state.prefState.statementIncludes.toList(), + menuItembuilder: (dynamic option) => Text( + localization.lookup(option), + style: TextStyle(fontSize: 14), + ), + isDense: true, + options: [ + kStatementIncludePayments, + kStatementIncludeCredits, + kStatementIncludeAging, + ], + whenEmpty: '', + )); /* final pageSelector = _pageCount == 1 @@ -218,104 +311,11 @@ class _ClientPdfViewState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - /* Expanded( child: Text( - EntityPresenter().initialize(client, context).title(), + EntityPresenter().initialize(client, context).title()!, ), ), - */ - Flexible( - child: Theme( - data: - state.prefState.enableDarkMode || state.hasAccentColor - ? ThemeData.dark() - : ThemeData.light(), - child: AppDropdownButton( - labelText: localization!.dateRange, - blankValue: null, - //showBlank: true, - value: _dateRange, - onChanged: (dynamic value) { - setState(() { - _dateRange = value; - }); - - if (value != DateRange.custom) { - loadPDF(); - } - }, - items: DateRange.values - .where((value) => value != DateRange.allTime) - .map((dateRange) => DropdownMenuItem( - child: Text(localization - .lookup(dateRange.toString())), - value: dateRange, - )) - .toList(), - ), - ), - ), - SizedBox(width: 16), - Flexible( - child: Theme( - data: - state.prefState.enableDarkMode || state.hasAccentColor - ? ThemeData.dark() - : ThemeData.light(), - child: AppDropdownButton( - labelText: localization.status, - blankValue: null, - value: _status, - onChanged: (dynamic value) { - setState(() { - _status = value; - }); - loadPDF(); - }, - items: [ - kStatementStatusAll, - kStatementStatusPaid, - kStatementStatusUnpaid, - ] - .map((value) => DropdownMenuItem( - child: Text(localization.lookup(value)), - value: value, - )) - .toList()), - ), - ), - if (isDesktop(context)) ...[ - Theme( - data: ThemeData( - appBarTheme: AppBarTheme( - titleTextStyle: TextStyle(fontSize: 60), - )), - child: Flexible( - child: DropDownMultiSelect( - onChanged: (List selected) { - //_selectedOptions = selected; - store.dispatch(UpdateUserPreferences( - statementIncludes: BuiltList(selected))); - loadPDF(); - }, - selectedValues: - state.prefState.statementIncludes.toList(), - menuItembuilder: (dynamic option) => Text( - localization.lookup(option), - style: TextStyle(fontSize: 14), - ), - isDense: true, - options: [ - kStatementIncludePayments, - kStatementIncludeCredits, - kStatementIncludeAging, - ], - whenEmpty: '', - )), - ) - //...pageSelector, - ] ], ), actions: [ @@ -327,7 +327,7 @@ class _ClientPdfViewState extends State { : () async { final fileName = localization.statement + '_' + - (client!.number) + + (client.number) + '.pdf'; saveDownloadedFile(_response!.bodyBytes, fileName); }, @@ -338,7 +338,7 @@ class _ClientPdfViewState extends State { onPressed: _response == null ? null : () async { - if (!client!.hasEmailAddress) { + if (!client.hasEmailAddress) { showMessageDialog( message: localization.clientEmailNotSet, secondaryActions: [ @@ -384,7 +384,7 @@ class _ClientPdfViewState extends State { entity: ScheduleEntity( ScheduleEntity.TEMPLATE_EMAIL_STATEMENT) .rebuild((b) => b - ..parameters.clients.add(client!.id) + ..parameters.clients.add(client.id) ..parameters.showAgingTable = includes.contains(localization.aging) ..parameters.showPaymentsTable = @@ -401,7 +401,7 @@ class _ClientPdfViewState extends State { child: Text(localization.close, style: TextStyle(color: state.headerTextColor)), onPressed: () { - viewEntity(entity: client!); + viewEntity(entity: client); }, ), ], @@ -409,6 +409,20 @@ class _ClientPdfViewState extends State { : null, body: Column( children: [ + Material( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: isDesktop(context) + ? Row( + children: [ + datePicker, + statusPicker, + sectionPicker, + ], + ) + : Placeholder(), + ), + ), if (_dateRange == DateRange.custom) Container( width: double.infinity, @@ -421,7 +435,7 @@ class _ClientPdfViewState extends State { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: DatePicker( - labelText: localization!.startDate, + labelText: localization.startDate, onSelected: (value, _) { setState(() { _startDate = value; @@ -457,15 +471,19 @@ class _ClientPdfViewState extends State { Expanded( child: _isLoading || _response == null ? LoadingIndicator() - : PdfPreview( - build: (format) => _response!.bodyBytes, - canChangeOrientation: false, - canChangePageFormat: false, - canDebug: false, - maxPageWidth: 600, - pdfFileName: - localization!.statement + '_' + client!.number + '.pdf', - ), + : (kIsWeb && state.prefState.enableNativeBrowser) + ? HtmlElementView(viewType: _pdfString!) + : PdfPreview( + build: (format) => _response!.bodyBytes, + canChangeOrientation: false, + canChangePageFormat: false, + canDebug: false, + maxPageWidth: 600, + pdfFileName: localization.statement + + '_' + + client.number + + '.pdf', + ), ), ], ), diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index b3f4d2ef1..97493d1f9 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -275,7 +275,6 @@ class _InvoicePdfViewState extends State { ) : null, body: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Material( child: Row( From 095944e8cc1627a6ed84276e40d48af8876f9110 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 9 Nov 2023 15:18:03 +0200 Subject: [PATCH 110/138] Add designs templates --- lib/ui/client/client_pdf.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/ui/client/client_pdf.dart b/lib/ui/client/client_pdf.dart index 5b5ea3876..da117db25 100644 --- a/lib/ui/client/client_pdf.dart +++ b/lib/ui/client/client_pdf.dart @@ -266,6 +266,7 @@ class _ClientPdfViewState extends State { kStatementIncludeAging, ], whenEmpty: '', + height: 50, )); /* @@ -416,7 +417,9 @@ class _ClientPdfViewState extends State { ? Row( children: [ datePicker, + SizedBox(width: 16), statusPicker, + SizedBox(width: 16), sectionPicker, ], ) From 891de33673d3c09bd76a9de00c3248d464cd22f6 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 9 Nov 2023 15:21:35 +0200 Subject: [PATCH 111/138] Add designs templates --- lib/ui/client/client_pdf.dart | 90 ++++++++++++++++------------------- 1 file changed, 40 insertions(+), 50 deletions(-) diff --git a/lib/ui/client/client_pdf.dart b/lib/ui/client/client_pdf.dart index da117db25..77d25e469 100644 --- a/lib/ui/client/client_pdf.dart +++ b/lib/ui/client/client_pdf.dart @@ -189,61 +189,51 @@ class _ClientPdfViewState extends State { final client = widget.viewModel.client!; final datePicker = Flexible( - child: Theme( - data: state.prefState.enableDarkMode || state.hasAccentColor - ? ThemeData.dark() - : ThemeData.light(), - child: AppDropdownButton( - labelText: localization.dateRange, - blankValue: null, - //showBlank: true, - value: _dateRange, - onChanged: (dynamic value) { - setState(() { - _dateRange = value; - }); + child: AppDropdownButton( + labelText: localization.dateRange, + blankValue: null, + //showBlank: true, + value: _dateRange, + onChanged: (dynamic value) { + setState(() { + _dateRange = value; + }); - if (value != DateRange.custom) { - loadPDF(); - } - }, - items: DateRange.values - .where((value) => value != DateRange.allTime) - .map((dateRange) => DropdownMenuItem( - child: Text(localization.lookup(dateRange.toString())), - value: dateRange, - )) - .toList(), - ), + if (value != DateRange.custom) { + loadPDF(); + } + }, + items: DateRange.values + .where((value) => value != DateRange.allTime) + .map((dateRange) => DropdownMenuItem( + child: Text(localization.lookup(dateRange.toString())), + value: dateRange, + )) + .toList(), ), ); final statusPicker = Flexible( - child: Theme( - data: state.prefState.enableDarkMode || state.hasAccentColor - ? ThemeData.dark() - : ThemeData.light(), - child: AppDropdownButton( - labelText: localization.status, - blankValue: null, - value: _status, - onChanged: (dynamic value) { - setState(() { - _status = value; - }); - loadPDF(); - }, - items: [ - kStatementStatusAll, - kStatementStatusPaid, - kStatementStatusUnpaid, - ] - .map((value) => DropdownMenuItem( - child: Text(localization.lookup(value)), - value: value, - )) - .toList()), - ), + child: AppDropdownButton( + labelText: localization.status, + blankValue: null, + value: _status, + onChanged: (dynamic value) { + setState(() { + _status = value; + }); + loadPDF(); + }, + items: [ + kStatementStatusAll, + kStatementStatusPaid, + kStatementStatusUnpaid, + ] + .map((value) => DropdownMenuItem( + child: Text(localization.lookup(value)), + value: value, + )) + .toList()), ); final sectionPicker = Flexible( From dafaa2c81d653e5f146976d716a178979d79a027 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 9 Nov 2023 16:46:29 +0200 Subject: [PATCH 112/138] Add designs templates --- lib/ui/client/client_pdf.dart | 47 ++++++++++++++++++++++----------- lib/ui/invoice/invoice_pdf.dart | 9 ++++--- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/lib/ui/client/client_pdf.dart b/lib/ui/client/client_pdf.dart index 77d25e469..e8dee1082 100644 --- a/lib/ui/client/client_pdf.dart +++ b/lib/ui/client/client_pdf.dart @@ -19,6 +19,7 @@ import 'package:invoiceninja_flutter/redux/settings/settings_actions.dart'; import 'package:invoiceninja_flutter/ui/app/buttons/elevated_button.dart'; import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart'; import 'package:invoiceninja_flutter/ui/app/forms/date_picker.dart'; +import 'package:invoiceninja_flutter/ui/app/forms/design_picker.dart'; import 'package:invoiceninja_flutter/ui/app/multiselect.dart'; import 'package:invoiceninja_flutter/ui/app/presenters/entity_presenter.dart'; import 'package:invoiceninja_flutter/utils/files.dart'; @@ -42,6 +43,8 @@ import 'package:invoiceninja_flutter/utils/platforms.dart'; import 'package:invoiceninja_flutter/utils/web_stub.dart' if (dart.library.html) 'package:invoiceninja_flutter/utils/web.dart'; +import '../../redux/design/design_selectors.dart'; + class ClientPdfView extends StatefulWidget { const ClientPdfView({ Key? key, @@ -68,15 +71,7 @@ class _ClientPdfViewState extends State { String? _endDate = convertDateTimeToSqlDate(); String _status = kStatementStatusAll; String? _pdfString; - - @override - void initState() { - super.initState(); - - //final state = widget.viewModel.state; - //final settings = state.dashboardUIState.settings; - //_dateRange = settings.dateRange; - } + String? _designId; @override void didChangeDependencies() { @@ -188,7 +183,25 @@ class _ClientPdfViewState extends State { final localization = AppLocalization.of(context)!; final client = widget.viewModel.client!; - final datePicker = Flexible( + final designPicker = Expanded( + child: IgnorePointer( + ignoring: _isLoading, + child: DesignPicker( + initialValue: _designId, + onSelected: (design) { + setState(() { + _designId = design?.id; + loadPDF(); + }); + }, + label: localization.design, + showBlank: true, + entityType: EntityType.client, + ), + ), + ); + + final datePicker = Expanded( child: AppDropdownButton( labelText: localization.dateRange, blankValue: null, @@ -213,7 +226,7 @@ class _ClientPdfViewState extends State { ), ); - final statusPicker = Flexible( + final statusPicker = Expanded( child: AppDropdownButton( labelText: localization.status, blankValue: null, @@ -236,7 +249,7 @@ class _ClientPdfViewState extends State { .toList()), ); - final sectionPicker = Flexible( + final sectionPicker = Expanded( child: DropDownMultiSelect( onChanged: (List selected) { //_selectedOptions = selected; @@ -399,6 +412,7 @@ class _ClientPdfViewState extends State { ) : null, body: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Material( child: Padding( @@ -406,6 +420,11 @@ class _ClientPdfViewState extends State { child: isDesktop(context) ? Row( children: [ + if (hasDesignTemplatesForEntityType( + state.designState.map, EntityType.client)) ...[ + designPicker, + SizedBox(width: 16), + ], datePicker, SizedBox(width: 16), statusPicker, @@ -417,9 +436,7 @@ class _ClientPdfViewState extends State { ), ), if (_dateRange == DateRange.custom) - Container( - width: double.infinity, - color: Theme.of(context).colorScheme.background, + Material( child: Wrap( alignment: WrapAlignment.center, children: [ diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index 97493d1f9..b021917e5 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -68,6 +68,7 @@ class _InvoicePdfViewState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); + loadPdf(); } @@ -143,7 +144,7 @@ class _InvoicePdfViewState extends State { ]; */ - final activitySelector = + final activityPicker = _activityId == null || (kIsWeb && state.prefState.enableNativeBrowser) ? SizedBox() : Expanded( @@ -177,7 +178,7 @@ class _InvoicePdfViewState extends State { ), ); - final designSelector = _activityId != null || + final designPicker = _activityId != null || (kIsWeb && state.prefState.enableNativeBrowser) || !hasDesignTemplatesForEntityType( state.designState.map, invoice.entityType!) @@ -279,8 +280,8 @@ class _InvoicePdfViewState extends State { Material( child: Row( children: [ - if (supportsDesignTemplates()) designSelector, - activitySelector, + if (supportsDesignTemplates()) designPicker, + activityPicker, if (invoice.isInvoice && _activityId == null) deliveryNote, ], ), From 911c95bd0a621c103157a37df9e437ec4972417c Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 9 Nov 2023 18:10:42 +0200 Subject: [PATCH 113/138] Fix for multiselect dialog --- lib/ui/app/dialogs/multiselect_dialog.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/ui/app/dialogs/multiselect_dialog.dart b/lib/ui/app/dialogs/multiselect_dialog.dart index bbf5e481a..5c5ab58e9 100644 --- a/lib/ui/app/dialogs/multiselect_dialog.dart +++ b/lib/ui/app/dialogs/multiselect_dialog.dart @@ -132,6 +132,7 @@ class MultiSelectListState extends State { mainAxisSize: MainAxisSize.min, children: [ AppDropdownButton( + key: ValueKey('__${selected.join(',')}__'), labelText: widget.addTitle, items: keys.map((option) { return DropdownMenuItem( From 589e9483a4abd8c376ad540b1154aaf5f9c9abe4 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 9 Nov 2023 18:17:02 +0200 Subject: [PATCH 114/138] Add designs templates --- lib/redux/design/design_selectors.dart | 5 +++++ lib/ui/client/client_pdf.dart | 22 +++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/redux/design/design_selectors.dart b/lib/redux/design/design_selectors.dart index 8ed988260..d0aa61419 100644 --- a/lib/redux/design/design_selectors.dart +++ b/lib/redux/design/design_selectors.dart @@ -1,5 +1,6 @@ // Package imports: import 'package:built_collection/built_collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:memoize/memoize.dart'; // Project imports: @@ -101,6 +102,10 @@ String? getDesignIdForVendorByEntity( bool hasDesignTemplatesForEntityType( BuiltMap designMap, EntityType entityType) { + if (!kReleaseMode) { + return true; + } + var hasMatch = false; designMap.forEach((designId, design) { diff --git a/lib/ui/client/client_pdf.dart b/lib/ui/client/client_pdf.dart index e8dee1082..5ddb7e522 100644 --- a/lib/ui/client/client_pdf.dart +++ b/lib/ui/client/client_pdf.dart @@ -432,7 +432,27 @@ class _ClientPdfViewState extends State { sectionPicker, ], ) - : Placeholder(), + : Column( + children: [ + Row( + children: [ + datePicker, + SizedBox(width: 16), + statusPicker, + ], + ), + Row( + children: [ + if (hasDesignTemplatesForEntityType( + state.designState.map, EntityType.client)) ...[ + designPicker, + SizedBox(width: 16), + ], + sectionPicker, + ], + ), + ], + ), ), ), if (_dateRange == DateRange.custom) From bd1d61960496e5ceee4ea3e7e3ef25e239863f52 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 9 Nov 2023 18:45:08 +0200 Subject: [PATCH 115/138] Add designs templates --- lib/ui/client/client_pdf.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/ui/client/client_pdf.dart b/lib/ui/client/client_pdf.dart index 5ddb7e522..0efcc8679 100644 --- a/lib/ui/client/client_pdf.dart +++ b/lib/ui/client/client_pdf.dart @@ -420,15 +420,15 @@ class _ClientPdfViewState extends State { child: isDesktop(context) ? Row( children: [ + datePicker, + SizedBox(width: 16), + statusPicker, + SizedBox(width: 16), if (hasDesignTemplatesForEntityType( state.designState.map, EntityType.client)) ...[ designPicker, SizedBox(width: 16), ], - datePicker, - SizedBox(width: 16), - statusPicker, - SizedBox(width: 16), sectionPicker, ], ) From a7de25323e2d0818a2e2ca6e84d8d4467e117024 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 9 Nov 2023 18:54:21 +0200 Subject: [PATCH 116/138] Add designs templates --- lib/redux/product/product_actions.dart | 3 +- .../purchase_order_middleware.dart | 3 +- .../recurring_invoice_middleware.dart | 3 +- lib/ui/client/client_pdf.dart | 37 ++----- .../edit/client_edit_shipping_address.dart | 2 +- .../invoice/edit/invoice_edit_contacts.dart | 3 +- lib/ui/invoice/invoice_pdf.dart | 103 ++++++++---------- 7 files changed, 58 insertions(+), 96 deletions(-) diff --git a/lib/redux/product/product_actions.dart b/lib/redux/product/product_actions.dart index 4d9d27213..a9fb2dc35 100644 --- a/lib/redux/product/product_actions.dart +++ b/lib/redux/product/product_actions.dart @@ -14,14 +14,13 @@ import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/document/document_actions.dart'; import 'package:invoiceninja_flutter/redux/product/product_selectors.dart'; import 'package:invoiceninja_flutter/ui/app/entities/entity_actions_dialog.dart'; import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/dialogs.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; -import '../document/document_actions.dart'; - class ViewProductList implements PersistUI { ViewProductList({ this.force = false, diff --git a/lib/redux/purchase_order/purchase_order_middleware.dart b/lib/redux/purchase_order/purchase_order_middleware.dart index 3219d24e2..fce9a6d29 100644 --- a/lib/redux/purchase_order/purchase_order_middleware.dart +++ b/lib/redux/purchase_order/purchase_order_middleware.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/data/repositories/purchase_order_repository.dart'; +import 'package:invoiceninja_flutter/redux/document/document_actions.dart'; import 'package:invoiceninja_flutter/redux/expense/expense_actions.dart'; import 'package:invoiceninja_flutter/redux/purchase_order/purchase_order_actions.dart'; import 'package:invoiceninja_flutter/ui/purchase_order/edit/purchase_order_edit_vm.dart'; @@ -20,8 +21,6 @@ import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/ui/ui_actions.dart'; -import '../document/document_actions.dart'; - List> createStorePurchaseOrdersMiddleware([ PurchaseOrderRepository repository = const PurchaseOrderRepository(), ]) { diff --git a/lib/redux/recurring_invoice/recurring_invoice_middleware.dart b/lib/redux/recurring_invoice/recurring_invoice_middleware.dart index c4e58525e..a756df161 100644 --- a/lib/redux/recurring_invoice/recurring_invoice_middleware.dart +++ b/lib/redux/recurring_invoice/recurring_invoice_middleware.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:invoiceninja_flutter/constants.dart'; +import 'package:invoiceninja_flutter/redux/document/document_actions.dart'; import 'package:invoiceninja_flutter/redux/payment/payment_actions.dart'; // Package imports: @@ -20,8 +21,6 @@ import 'package:invoiceninja_flutter/ui/recurring_invoice/recurring_invoice_pdf_ import 'package:invoiceninja_flutter/ui/recurring_invoice/recurring_invoice_screen.dart'; import 'package:invoiceninja_flutter/ui/recurring_invoice/view/recurring_invoice_view_vm.dart'; -import '../document/document_actions.dart'; - List> createStoreRecurringInvoicesMiddleware([ RecurringInvoiceRepository repository = const RecurringInvoiceRepository(), ]) { diff --git a/lib/ui/client/client_pdf.dart b/lib/ui/client/client_pdf.dart index 0efcc8679..38447cfe8 100644 --- a/lib/ui/client/client_pdf.dart +++ b/lib/ui/client/client_pdf.dart @@ -4,7 +4,6 @@ import 'dart:convert'; // Flutter imports: import 'package:built_collection/built_collection.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; // Package imports: @@ -15,6 +14,7 @@ import 'package:http/http.dart'; import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/main_app.dart'; +import 'package:invoiceninja_flutter/redux/design/design_selectors.dart'; import 'package:invoiceninja_flutter/redux/settings/settings_actions.dart'; import 'package:invoiceninja_flutter/ui/app/buttons/elevated_button.dart'; import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart'; @@ -40,11 +40,6 @@ import 'package:invoiceninja_flutter/utils/dialogs.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart'; -import 'package:invoiceninja_flutter/utils/web_stub.dart' - if (dart.library.html) 'package:invoiceninja_flutter/utils/web.dart'; - -import '../../redux/design/design_selectors.dart'; - class ClientPdfView extends StatefulWidget { const ClientPdfView({ Key? key, @@ -70,7 +65,6 @@ class _ClientPdfViewState extends State { convertDateTimeToSqlDate(DateTime.now().subtract(Duration(days: 365))); String? _endDate = convertDateTimeToSqlDate(); String _status = kStatementStatusAll; - String? _pdfString; String? _designId; @override @@ -86,7 +80,6 @@ class _ClientPdfViewState extends State { } final localization = AppLocalization.of(context); - final state = widget.viewModel.state; setState(() { _isLoading = true; @@ -100,12 +93,6 @@ class _ClientPdfViewState extends State { } } else { _response = response; - - if (kIsWeb && state.prefState.enableNativeBrowser) { - _pdfString = 'data:application/pdf;base64,' + - base64Encode(response!.bodyBytes); - WebUtils.registerWebView(_pdfString); - } } _isLoading = false; @@ -501,19 +488,15 @@ class _ClientPdfViewState extends State { Expanded( child: _isLoading || _response == null ? LoadingIndicator() - : (kIsWeb && state.prefState.enableNativeBrowser) - ? HtmlElementView(viewType: _pdfString!) - : PdfPreview( - build: (format) => _response!.bodyBytes, - canChangeOrientation: false, - canChangePageFormat: false, - canDebug: false, - maxPageWidth: 600, - pdfFileName: localization.statement + - '_' + - client.number + - '.pdf', - ), + : PdfPreview( + build: (format) => _response!.bodyBytes, + canChangeOrientation: false, + canChangePageFormat: false, + canDebug: false, + maxPageWidth: 600, + pdfFileName: + localization.statement + '_' + client.number + '.pdf', + ), ), ], ), diff --git a/lib/ui/client/edit/client_edit_shipping_address.dart b/lib/ui/client/edit/client_edit_shipping_address.dart index 2bd16d0e8..1262ec3c1 100644 --- a/lib/ui/client/edit/client_edit_shipping_address.dart +++ b/lib/ui/client/edit/client_edit_shipping_address.dart @@ -7,11 +7,11 @@ import 'package:invoiceninja_flutter/data/models/entities.dart'; import 'package:invoiceninja_flutter/redux/static/static_selectors.dart'; import 'package:invoiceninja_flutter/ui/app/buttons/elevated_button.dart'; 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/decorated_form_field.dart'; import 'package:invoiceninja_flutter/ui/client/edit/client_edit_vm.dart'; import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; -import '../../app/form_card.dart'; class ClientEditShippingAddress extends StatefulWidget { const ClientEditShippingAddress({ diff --git a/lib/ui/invoice/edit/invoice_edit_contacts.dart b/lib/ui/invoice/edit/invoice_edit_contacts.dart index 647baca97..e8c392756 100644 --- a/lib/ui/invoice/edit/invoice_edit_contacts.dart +++ b/lib/ui/invoice/edit/invoice_edit_contacts.dart @@ -6,14 +6,13 @@ import 'package:flutter_styled_toast/flutter_styled_toast.dart'; // Project imports: import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/ui/app/help_text.dart'; import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart'; import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_contacts_vm.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; -import '../../../redux/app/app_state.dart'; - class InvoiceEditContacts extends StatelessWidget { const InvoiceEditContacts({ Key? key, diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index b021917e5..a2281f1cb 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -1,6 +1,5 @@ // Dart imports: import 'dart:async'; -import 'dart:convert'; // Flutter imports: import 'package:flutter/foundation.dart'; @@ -31,9 +30,6 @@ import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart'; -import 'package:invoiceninja_flutter/utils/web_stub.dart' - if (dart.library.html) 'package:invoiceninja_flutter/utils/web.dart'; - class InvoicePdfView extends StatefulWidget { const InvoicePdfView({ Key? key, @@ -53,7 +49,6 @@ class _InvoicePdfViewState extends State { bool _isDeliveryNote = false; String? _designId; String? _activityId; - String? _pdfString; http.Response? _response; //int _pageCount = 1; //int _currentPage = 1; @@ -75,7 +70,6 @@ class _InvoicePdfViewState extends State { void loadPdf() { final viewModel = widget.viewModel; final invoice = viewModel.invoice!; - final state = viewModel.state; if (invoice.invitations.isEmpty) { return; @@ -95,12 +89,6 @@ class _InvoicePdfViewState extends State { setState(() { _response = response; _isLoading = false; - - if (kIsWeb && state!.prefState.enableNativeBrowser) { - _pdfString = 'data:application/pdf;base64,' + - base64Encode(response!.bodyBytes); - WebUtils.registerWebView(_pdfString); - } }); }).catchError((Object error) { setState(() { @@ -144,42 +132,39 @@ class _InvoicePdfViewState extends State { ]; */ - final activityPicker = - _activityId == null || (kIsWeb && state.prefState.enableNativeBrowser) - ? SizedBox() - : Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 17), - child: IgnorePointer( - ignoring: _isLoading, - child: AppDropdownButton( - value: _activityId, - onChanged: (dynamic activityId) { - setState(() { - _activityId = activityId; - loadPdf(); - }); - }, - items: invoice.balanceHistory - .map((history) => DropdownMenuItem( - child: Text(formatNumber( - history.amount, context, - clientId: invoice.clientId)! + - ' • ' + - formatDate( - convertTimestampToDateString( - history.createdAt), - context, - showTime: true)), - value: history.activityId, - )) - .toList()), - ), - ), - ); + final activityPicker = _activityId == null + ? SizedBox() + : Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 17), + child: IgnorePointer( + ignoring: _isLoading, + child: AppDropdownButton( + value: _activityId, + onChanged: (dynamic activityId) { + setState(() { + _activityId = activityId; + loadPdf(); + }); + }, + items: invoice.balanceHistory + .map((history) => DropdownMenuItem( + child: Text(formatNumber(history.amount, context, + clientId: invoice.clientId)! + + ' • ' + + formatDate( + convertTimestampToDateString( + history.createdAt), + context, + showTime: true)), + value: history.activityId, + )) + .toList()), + ), + ), + ); final designPicker = _activityId != null || - (kIsWeb && state.prefState.enableNativeBrowser) || !hasDesignTemplatesForEntityType( state.designState.map, invoice.entityType!) ? SizedBox() @@ -289,20 +274,18 @@ class _InvoicePdfViewState extends State { Expanded( child: _isLoading || _response == null ? LoadingIndicator() - : (kIsWeb && state.prefState.enableNativeBrowser) - ? HtmlElementView(viewType: _pdfString!) - : PdfPreview( - build: (format) => _response!.bodyBytes, - canChangeOrientation: false, - canChangePageFormat: false, - canDebug: false, - maxPageWidth: 600, - pdfFileName: - localization.lookup(invoice.entityType!.snakeCase) + - '_' + - invoice.number + - '.pdf', - ), + : PdfPreview( + build: (format) => _response!.bodyBytes, + canChangeOrientation: false, + canChangePageFormat: false, + canDebug: false, + maxPageWidth: 600, + pdfFileName: + localization.lookup(invoice.entityType!.snakeCase) + + '_' + + invoice.number + + '.pdf', + ), ), ], ), From fd1d6426ad4fac7beb399eb5be412438b21a4fae Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 9 Nov 2023 18:58:21 +0200 Subject: [PATCH 117/138] Add designs templates --- lib/ui/client/client_pdf.dart | 6 +++++- lib/ui/invoice/invoice_pdf.dart | 10 ++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/ui/client/client_pdf.dart b/lib/ui/client/client_pdf.dart index 38447cfe8..7a4856e53 100644 --- a/lib/ui/client/client_pdf.dart +++ b/lib/ui/client/client_pdf.dart @@ -110,7 +110,8 @@ class _ClientPdfViewState extends State { }); } - Future _loadPDF({bool sendEmail = false}) async { + Future _loadPDF( + {bool sendEmail = false, String designId = ''}) async { final client = widget.viewModel.client!; http.Response? response; @@ -122,6 +123,9 @@ class _ClientPdfViewState extends State { if (sendEmail) { url += '?send_email=true'; } + if (designId.isNotEmpty) { + url += '&design_id=$designId'; + } String? startDate = ''; String? endDate = ''; diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index a2281f1cb..45f56baef 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -307,14 +307,20 @@ Future _loadPDF( if ((activityId ?? '').isNotEmpty || isDeliveryNote) { final credential = store.state.credentials; - final url = isDeliveryNote + var url = isDeliveryNote ? '/invoices/${invoice.id}/delivery_note' : '/activities/download_entity/$activityId'; + if ((designId ?? '').isNotEmpty) { + url += '&design_id=$designId'; + } response = await WebClient() .get('${credential.url}$url', credential.token, rawResponse: true); } else { final invitation = invoice.invitations.first; - final url = invitation.downloadLink; + var url = invitation.downloadLink; + if ((designId ?? '').isNotEmpty) { + url += '&design_id=$designId'; + } response = await WebClient().get(url, state.token, rawResponse: true); } From e482cc3de3d5d0b7f8faa18dc8b4d0d2a6fc8180 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 9 Nov 2023 19:02:39 +0200 Subject: [PATCH 118/138] Add designs templates --- lib/ui/invoice/invoice_pdf.dart | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index 45f56baef..db3fd7602 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -300,29 +300,26 @@ Future _loadPDF( String? activityId, String? designId, ) async { - http.Response? response; - final store = StoreProvider.of(context); final state = store.state; + final credential = store.state.credentials; + final invitation = invoice.invitations.first; - if ((activityId ?? '').isNotEmpty || isDeliveryNote) { - final credential = store.state.credentials; - var url = isDeliveryNote - ? '/invoices/${invoice.id}/delivery_note' - : '/activities/download_entity/$activityId'; - if ((designId ?? '').isNotEmpty) { - url += '&design_id=$designId'; - } - response = await WebClient() - .get('${credential.url}$url', credential.token, rawResponse: true); + var url = ''; + + if ((activityId ?? '').isNotEmpty) { + url = '${credential.url}/activities/download_entity/$activityId'; } else { - final invitation = invoice.invitations.first; - var url = invitation.downloadLink; + if (isDeliveryNote) { + url = '${credential.url}/invoices/${invoice.id}/delivery_note'; + } else { + url = invitation.downloadLink; + } + if ((designId ?? '').isNotEmpty) { url += '&design_id=$designId'; } - response = await WebClient().get(url, state.token, rawResponse: true); } - return response; + return await WebClient().get(url, state.token, rawResponse: true); } From e1c83798089d03e90d3dfa4d07bceb19db4f0a6c Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 9 Nov 2023 19:07:47 +0200 Subject: [PATCH 119/138] Add designs templates --- lib/ui/client/client_pdf.dart | 133 ++++++++++++++++---------------- lib/ui/invoice/invoice_pdf.dart | 21 ++--- 2 files changed, 80 insertions(+), 74 deletions(-) diff --git a/lib/ui/client/client_pdf.dart b/lib/ui/client/client_pdf.dart index 7a4856e53..c32a1c301 100644 --- a/lib/ui/client/client_pdf.dart +++ b/lib/ui/client/client_pdf.dart @@ -407,33 +407,17 @@ class _ClientPdfViewState extends State { children: [ Material( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: isDesktop(context) - ? Row( - children: [ - datePicker, - SizedBox(width: 16), - statusPicker, - SizedBox(width: 16), - if (hasDesignTemplatesForEntityType( - state.designState.map, EntityType.client)) ...[ - designPicker, - SizedBox(width: 16), - ], - sectionPicker, - ], - ) - : Column( - children: [ - Row( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + isDesktop(context) + ? Row( children: [ datePicker, SizedBox(width: 16), statusPicker, - ], - ), - Row( - children: [ + SizedBox(width: 16), if (hasDesignTemplatesForEntityType( state.designState.map, EntityType.client)) ...[ designPicker, @@ -441,54 +425,73 @@ class _ClientPdfViewState extends State { ], sectionPicker, ], + ) + : Column( + children: [ + Row( + children: [ + datePicker, + SizedBox(width: 16), + statusPicker, + ], + ), + Row( + children: [ + if (hasDesignTemplatesForEntityType( + state.designState.map, + EntityType.client)) ...[ + designPicker, + SizedBox(width: 16), + ], + sectionPicker, + ], + ), + ], ), + if (_dateRange == DateRange.custom) ...[ + SizedBox(height: 8), + Wrap( + alignment: WrapAlignment.start, + children: [ + Container( + width: 180, + child: DatePicker( + labelText: localization.startDate, + onSelected: (value, _) { + setState(() { + _startDate = value; + }); + }, + selectedDate: _startDate, + ), + ), + SizedBox(width: 16), + Container( + width: 180, + child: DatePicker( + labelText: localization.endDate, + onSelected: (value, _) { + setState(() { + _endDate = value; + }); + }, + selectedDate: _endDate, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: AppButton( + label: localization.loadPdf, + onPressed: () => loadPDF(), + ), + ) ], ), - ), - ), - if (_dateRange == DateRange.custom) - Material( - child: Wrap( - alignment: WrapAlignment.center, - children: [ - Container( - width: 180, - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: DatePicker( - labelText: localization.startDate, - onSelected: (value, _) { - setState(() { - _startDate = value; - }); - }, - selectedDate: _startDate, - ), - ), - Container( - width: 180, - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: DatePicker( - labelText: localization.endDate, - onSelected: (value, _) { - setState(() { - _endDate = value; - }); - }, - selectedDate: _endDate, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: AppButton( - label: localization.loadPdf, - onPressed: () => loadPDF(), - ), - ) + ], ], ), ), + ), Expanded( child: _isLoading || _response == null ? LoadingIndicator() diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index db3fd7602..c9bce872a 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -263,12 +263,15 @@ class _InvoicePdfViewState extends State { body: Column( children: [ Material( - child: Row( - children: [ - if (supportsDesignTemplates()) designPicker, - activityPicker, - if (invoice.isInvoice && _activityId == null) deliveryNote, - ], + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + if (supportsDesignTemplates()) designPicker, + activityPicker, + if (invoice.isInvoice && _activityId == null) deliveryNote, + ], + ), ), ), Expanded( @@ -302,16 +305,16 @@ Future _loadPDF( ) async { final store = StoreProvider.of(context); final state = store.state; - final credential = store.state.credentials; + final credentials = store.state.credentials; final invitation = invoice.invitations.first; var url = ''; if ((activityId ?? '').isNotEmpty) { - url = '${credential.url}/activities/download_entity/$activityId'; + url = '${credentials.url}/activities/download_entity/$activityId'; } else { if (isDeliveryNote) { - url = '${credential.url}/invoices/${invoice.id}/delivery_note'; + url = '${credentials.url}/invoices/${invoice.id}/delivery_note'; } else { url = invitation.downloadLink; } From e0f6fbdd9c78bc2115526ef557d83294a53484b6 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 12 Nov 2023 09:23:49 +0200 Subject: [PATCH 120/138] Email Templates and Reminders #606 --- lib/utils/markdown.dart | 7 +++---- lib/utils/super_editor/super_editor.dart | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/utils/markdown.dart b/lib/utils/markdown.dart index 2ddf55ff1..0c942c618 100644 --- a/lib/utils/markdown.dart +++ b/lib/utils/markdown.dart @@ -1,5 +1,5 @@ -/* // DELETE THIS FILE ONCE SUPER EDITOR IS UPDATED +// Note: using the standard function crashes with h1 tags import 'dart:convert'; @@ -468,7 +468,7 @@ class AttributedTextMarkdownSerializer extends AttributionVisitor { final linkMarker = _encodeLinkMarker(startingAttributions, AttributionVisitEvent.start); - _buffer + _buffer! ..write(linkMarker) ..write(markdownStyles); } @@ -488,7 +488,7 @@ class AttributedTextMarkdownSerializer extends AttributionVisitor { // +1 on end index because this visitor has inclusive indices // whereas substring() expects an exclusive ending index. - _buffer + _buffer! ..write(markdownStyles) ..write(linkMarker); } @@ -588,4 +588,3 @@ class _EmptyParagraphSyntax extends md.BlockSyntax { return md.Element('p', []); } } -*/ diff --git a/lib/utils/super_editor/super_editor.dart b/lib/utils/super_editor/super_editor.dart index 03320a85b..c4f0f17a1 100644 --- a/lib/utils/super_editor/super_editor.dart +++ b/lib/utils/super_editor/super_editor.dart @@ -1,8 +1,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/utils/markdown.dart'; import 'package:invoiceninja_flutter/utils/super_editor/toolbar.dart'; import 'package:super_editor/super_editor.dart'; -import 'package:super_editor_markdown/super_editor_markdown.dart'; /// Example of a rich text editor. /// From 33876d8f14cc07bcae9e09b8d9ee90eb095f1392 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 12 Nov 2023 09:25:02 +0200 Subject: [PATCH 121/138] Hide downloads folder from new settings list on web --- lib/ui/settings/settings_list.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/ui/settings/settings_list.dart b/lib/ui/settings/settings_list.dart index 7ac728f1b..5ac8fe2e2 100644 --- a/lib/ui/settings/settings_list.dart +++ b/lib/ui/settings/settings_list.dart @@ -1,4 +1,5 @@ // Flutter imports: +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; // Package imports: @@ -505,7 +506,7 @@ class SettingsSearch extends StatelessWidget { 'show_pdf_preview', 'pdf_preview_location#2022-10-24', 'refresh_data', - 'downloads_folder#2023-10-29' + if (!kIsWeb) 'downloads_folder#2023-10-29' ], [ 'dark_mode', From ac9ad1207705326ae2851665497e113cf1a81f9d Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 12 Nov 2023 09:28:26 +0200 Subject: [PATCH 122/138] Show custom CSS to hosted users --- lib/ui/settings/client_portal.dart | 15 +++++++-------- lib/ui/settings/settings_list.dart | 6 ++---- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/lib/ui/settings/client_portal.dart b/lib/ui/settings/client_portal.dart index 2763e3f76..2a9ccdab9 100644 --- a/lib/ui/settings/client_portal.dart +++ b/lib/ui/settings/client_portal.dart @@ -731,20 +731,19 @@ class _ClientPortalState extends State maxLines: 6, keyboardType: TextInputType.multiline, ), - if (isSelfHosted(context)) ...[ - DecoratedFormField( - label: localization.customCss, - controller: _customCssController, - maxLines: 6, - keyboardType: TextInputType.multiline, - ), + DecoratedFormField( + label: localization.customCss, + controller: _customCssController, + maxLines: 6, + keyboardType: TextInputType.multiline, + ), + if (isSelfHosted(context)) DecoratedFormField( label: localization.customJavascript, controller: _customJavaScriptController, maxLines: 6, keyboardType: TextInputType.multiline, ), - ], ], ) ], diff --git a/lib/ui/settings/settings_list.dart b/lib/ui/settings/settings_list.dart index 5ac8fe2e2..b88c5364e 100644 --- a/lib/ui/settings/settings_list.dart +++ b/lib/ui/settings/settings_list.dart @@ -609,10 +609,8 @@ class SettingsSearch extends StatelessWidget { [ 'header', 'footer', - if (isSelfHosted(context)) ...[ - 'custom_css', - 'custom_javascript', - ], + 'custom_css', + if (isSelfHosted(context)) 'custom_javascript', ], ], kSettingsEmailSettings: [ From 4e689254f863cb085712a243d0821721d7d99710 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 12 Nov 2023 09:33:20 +0200 Subject: [PATCH 123/138] Fix for color picker --- lib/ui/app/forms/color_picker.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ui/app/forms/color_picker.dart b/lib/ui/app/forms/color_picker.dart index cb079a720..23138d8d2 100644 --- a/lib/ui/app/forms/color_picker.dart +++ b/lib/ui/app/forms/color_picker.dart @@ -43,13 +43,13 @@ class _FormColorPickerState extends State { final _debouncer = Debouncer(); late List _controllers; - final _defaultColors = [ + final _defaultColors = [ Colors.red, Colors.pink, Colors.purple, Colors.deepPurple, Colors.indigo, - convertHexStringToColor(kDefaultAccentColor), + convertHexStringToColor(kDefaultAccentColor) ?? Colors.black, Colors.blue, Colors.lightBlue, Colors.cyan, @@ -133,7 +133,7 @@ class _FormColorPickerState extends State { content: SingleChildScrollView( child: BlockPicker( availableColors: [ - ..._defaultColors as Iterable, + ..._defaultColors, colors!.colorInfo!, colors.colorPrimary!, colors.colorSuccess!, From bc54291b38f8f6a6edf08cf08a4b8daba04b27dc Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 12 Nov 2023 09:34:38 +0200 Subject: [PATCH 124/138] Update version --- .github/workflows/flatpak.yml | 2 +- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 1 + lib/constants.dart | 2 +- pubspec.foss.yaml | 2 +- pubspec.yaml | 2 +- snap/snapcraft.yaml | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index a37bbf50a..4752ebfca 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -86,7 +86,7 @@ jobs: draft: false prerelease: false title: "Latest Release" - automatic_release_tag: "v5.0.138" + automatic_release_tag: "v5.0.139" files: | ${{ github.workspace }}/artifacts/Invoice-Ninja-Archive ${{ github.workspace }}/artifacts/Invoice-Ninja-Hash diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index 47c4eb37c..ba6fb785c 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -50,6 +50,7 @@ + diff --git a/lib/constants.dart b/lib/constants.dart index f9c90a28a..5c8eb3aa5 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -4,7 +4,7 @@ class Constants { } // TODO remove version once #46609 is fixed -const String kClientVersion = '5.0.138'; +const String kClientVersion = '5.0.139'; const String kMinServerVersion = '5.0.4'; const String kAppName = 'Invoice Ninja'; diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index d325990aa..9d7c93e06 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.138+138 +version: 5.0.139+139 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/pubspec.yaml b/pubspec.yaml index f5ccc9cfd..6e0ffbf78 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.138+138 +version: 5.0.139+139 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 49de8166c..3726cf8bc 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: invoiceninja -version: '5.0.138' +version: '5.0.139' summary: Create invoices, accept payments, track expenses & time tasks description: "### Note: if the app fails to run using `snap run invoiceninja` it may help to run `/snap/invoiceninja/current/bin/invoiceninja` instead From ba34b14066b5c9f2319b010854e8bd69db41c89c Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 12 Nov 2023 10:14:22 +0200 Subject: [PATCH 125/138] Fix for default payment currency setting --- lib/ui/payment/edit/payment_edit.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ui/payment/edit/payment_edit.dart b/lib/ui/payment/edit/payment_edit.dart index a439ef036..ce0cea158 100644 --- a/lib/ui/payment/edit/payment_edit.dart +++ b/lib/ui/payment/edit/payment_edit.dart @@ -81,7 +81,7 @@ class _PaymentEditState extends State { _showConvertCurrency = payment.exchangeRate != 1 && payment.exchangeRate != 0; final state = widget.viewModel.state; - if (state.company.convertExpenseCurrency) { + if (state.company.convertPaymentCurrency) { _showConvertCurrency = true; } From 6f743974ce9bd3e238a216fe9442e07ec7325013 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 12 Nov 2023 11:38:52 +0200 Subject: [PATCH 126/138] Rollback Google Sign In package --- pubspec.lock | 16 ++++------------ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 6d4e2e58c..df00e5920 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -544,22 +544,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" - google_identity_services_web: - dependency: transitive - description: - name: google_identity_services_web - sha256: "554748f2478619076128152c58905620d10f9c7fc270ff1d3a9675f9f53838ed" - url: "https://pub.dev" - source: hosted - version: "0.2.1+1" google_sign_in: dependency: "direct main" description: name: google_sign_in - sha256: f45038d27bcad37498f282295ae97eece23c9349fc16649154067b87b9f1fd03 + sha256: "821f354c053d51a2d417b02d42532a19a6ea8057d2f9ebb8863c07d81c98aaf9" url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "5.4.4" google_sign_in_android: dependency: transitive description: @@ -588,10 +580,10 @@ packages: dependency: transitive description: name: google_sign_in_web - sha256: "939e9172a378ec4eaeb7f71eeddac9b55ebd0e8546d336daec476a68e5279766" + sha256: "75cc41ebc53b1756320ee14d9c3018ad3e6cea298147dbcd86e9d0c8d6720b40" url: "https://pub.dev" source: hosted - version: "0.12.0+5" + version: "0.10.2+1" graphs: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6e0ffbf78..83853793a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: flutter_localizations: sdk: flutter #google_sign_in: ^6.0.1 - google_sign_in: ^6.1.5 + google_sign_in: 5.4.4 #https://pub.dev/packages/google_sign_in_web in_app_review: ^2.0.4 in_app_purchase: ^3.1.1 pinput: ^3.0.1 From 59095a1c9def05c305e3aff8a18c1719693cc67b Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 12 Nov 2023 11:40:52 +0200 Subject: [PATCH 127/138] Don't show delivery note option when emailing --- lib/ui/invoice/invoice_pdf.dart | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index c9bce872a..a33cf2fe8 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -262,18 +262,19 @@ class _InvoicePdfViewState extends State { : null, body: Column( children: [ - Material( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - children: [ - if (supportsDesignTemplates()) designPicker, - activityPicker, - if (invoice.isInvoice && _activityId == null) deliveryNote, - ], + if (widget.showAppBar) + Material( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + if (supportsDesignTemplates()) designPicker, + activityPicker, + if (invoice.isInvoice && _activityId == null) deliveryNote, + ], + ), ), ), - ), Expanded( child: _isLoading || _response == null ? LoadingIndicator() From be0a7d7ecce693d2509814e1b2d824c49e1a45e3 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 12 Nov 2023 13:04:16 +0200 Subject: [PATCH 128/138] Hide email error if delivered --- lib/ui/invoice/edit/invoice_edit_contacts.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/ui/invoice/edit/invoice_edit_contacts.dart b/lib/ui/invoice/edit/invoice_edit_contacts.dart index e8c392756..0b177d8bd 100644 --- a/lib/ui/invoice/edit/invoice_edit_contacts.dart +++ b/lib/ui/invoice/edit/invoice_edit_contacts.dart @@ -276,7 +276,9 @@ class _VendorContactListTile extends StatelessWidget { style: Theme.of(context).textTheme.bodySmall, ), ), - if ((invitation?.emailError ?? '').isNotEmpty) + if ((invitation?.emailError ?? '').isNotEmpty && + invitation?.emailStatus != + InvitationEntity.EMAIL_STATUS_DELIVERED) Padding( padding: const EdgeInsets.only(top: 8), child: Text( From 9ce3970f3fa3b927cd1b2f14ade2afad8dbc420e Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 12 Nov 2023 22:07:53 +0200 Subject: [PATCH 129/138] Hide email delivery details if not an error --- lib/ui/invoice/edit/invoice_edit_contacts.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/ui/invoice/edit/invoice_edit_contacts.dart b/lib/ui/invoice/edit/invoice_edit_contacts.dart index 0b177d8bd..05af4e832 100644 --- a/lib/ui/invoice/edit/invoice_edit_contacts.dart +++ b/lib/ui/invoice/edit/invoice_edit_contacts.dart @@ -181,7 +181,9 @@ class _ClientContactListTile extends StatelessWidget { style: Theme.of(context).textTheme.bodySmall, ), ), - if ((invitation?.emailError ?? '').isNotEmpty) + if ((invitation?.emailError ?? '').isNotEmpty && + invitation?.emailStatus != + InvitationEntity.EMAIL_STATUS_DELIVERED) Padding( padding: const EdgeInsets.only(top: 8), child: Text( From 4bff54fc1b77a950c78f1af4287d65cf72fe7650 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 12 Nov 2023 22:08:05 +0200 Subject: [PATCH 130/138] Reactivate bounced emails --- lib/data/models/invoice_model.dart | 7 ++++++- lib/data/models/invoice_model.g.dart | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/data/models/invoice_model.dart b/lib/data/models/invoice_model.dart index ef3192008..b8e2f4246 100644 --- a/lib/data/models/invoice_model.dart +++ b/lib/data/models/invoice_model.dart @@ -1794,6 +1794,7 @@ abstract class InvitationEntity extends Object sentDate: '', viewedDate: '', openedDate: '', + messageId: '', updatedAt: 0, archivedAt: 0, isDeleted: false, @@ -1835,6 +1836,9 @@ abstract class InvitationEntity extends Object @BuiltValueField(wireName: 'email_error', compare: false) String get emailError; + @BuiltValueField(wireName: 'message_id', compare: false) + String get messageId; + String get downloadLink => '$link/download?t=${DateTime.now().millisecondsSinceEpoch}'; @@ -1903,7 +1907,8 @@ abstract class InvitationEntity extends Object ..clientContactId = '' ..vendorContactId = '' ..emailError = '' - ..emailStatus = ''; + ..emailStatus = '' + ..messageId = ''; static Serializer get serializer => _$invitationEntitySerializer; diff --git a/lib/data/models/invoice_model.g.dart b/lib/data/models/invoice_model.g.dart index f4f693205..bc63bedab 100644 --- a/lib/data/models/invoice_model.g.dart +++ b/lib/data/models/invoice_model.g.dart @@ -983,6 +983,9 @@ class _$InvitationEntitySerializer 'email_error', serializers.serialize(object.emailError, specifiedType: const FullType(String)), + 'message_id', + serializers.serialize(object.messageId, + specifiedType: const FullType(String)), 'created_at', serializers.serialize(object.createdAt, specifiedType: const FullType(int)), @@ -1082,6 +1085,10 @@ class _$InvitationEntitySerializer result.emailError = serializers.deserialize(value, specifiedType: const FullType(String))! as String; break; + case 'message_id': + result.messageId = serializers.deserialize(value, + specifiedType: const FullType(String))! as String; + break; case 'isChanged': result.isChanged = serializers.deserialize(value, specifiedType: const FullType(bool)) as bool?; @@ -2986,6 +2993,8 @@ class _$InvitationEntity extends InvitationEntity { @override final String emailError; @override + final String messageId; + @override final bool? isChanged; @override final int createdAt; @@ -3018,6 +3027,7 @@ class _$InvitationEntity extends InvitationEntity { required this.openedDate, required this.emailStatus, required this.emailError, + required this.messageId, this.isChanged, required this.createdAt, required this.updatedAt, @@ -3044,6 +3054,8 @@ class _$InvitationEntity extends InvitationEntity { emailStatus, r'InvitationEntity', 'emailStatus'); BuiltValueNullFieldError.checkNotNull( emailError, r'InvitationEntity', 'emailError'); + BuiltValueNullFieldError.checkNotNull( + messageId, r'InvitationEntity', 'messageId'); BuiltValueNullFieldError.checkNotNull( createdAt, r'InvitationEntity', 'createdAt'); BuiltValueNullFieldError.checkNotNull( @@ -3114,6 +3126,7 @@ class _$InvitationEntity extends InvitationEntity { ..add('openedDate', openedDate) ..add('emailStatus', emailStatus) ..add('emailError', emailError) + ..add('messageId', messageId) ..add('isChanged', isChanged) ..add('createdAt', createdAt) ..add('updatedAt', updatedAt) @@ -3169,6 +3182,10 @@ class InvitationEntityBuilder String? get emailError => _$this._emailError; set emailError(String? emailError) => _$this._emailError = emailError; + String? _messageId; + String? get messageId => _$this._messageId; + set messageId(String? messageId) => _$this._messageId = messageId; + bool? _isChanged; bool? get isChanged => _$this._isChanged; set isChanged(bool? isChanged) => _$this._isChanged = isChanged; @@ -3223,6 +3240,7 @@ class InvitationEntityBuilder _openedDate = $v.openedDate; _emailStatus = $v.emailStatus; _emailError = $v.emailError; + _messageId = $v.messageId; _isChanged = $v.isChanged; _createdAt = $v.createdAt; _updatedAt = $v.updatedAt; @@ -3272,6 +3290,7 @@ class InvitationEntityBuilder emailStatus, r'InvitationEntity', 'emailStatus'), emailError: BuiltValueNullFieldError.checkNotNull(emailError, r'InvitationEntity', 'emailError'), + messageId: BuiltValueNullFieldError.checkNotNull(messageId, r'InvitationEntity', 'messageId'), isChanged: isChanged, createdAt: BuiltValueNullFieldError.checkNotNull(createdAt, r'InvitationEntity', 'createdAt'), updatedAt: BuiltValueNullFieldError.checkNotNull(updatedAt, r'InvitationEntity', 'updatedAt'), From 46293cf7a6952c1a1f6590184f3fc57d0eb1b282 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 13 Nov 2023 13:36:52 +0200 Subject: [PATCH 131/138] Android build --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index ae90d44bb..177e330d0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -25,6 +25,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } From 6b7f1ac8608a3e353cfde197c6e32aa5b03e18c7 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 14 Nov 2023 08:10:00 +0200 Subject: [PATCH 132/138] Fix for download link --- lib/redux/credit/credit_actions.dart | 3 +-- lib/redux/purchase_order/purchase_order_actions.dart | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/redux/credit/credit_actions.dart b/lib/redux/credit/credit_actions.dart index 62b0550b4..83d1b4599 100644 --- a/lib/redux/credit/credit_actions.dart +++ b/lib/redux/credit/credit_actions.dart @@ -631,8 +631,7 @@ Future handleCreditAction(BuildContext context, List credits, case EntityAction.download: store.dispatch(StartLoading()); await WebClient() - .get(credit.invitationEInvoiceDownloadLink, state.token, - rawResponse: true) + .get(credit.invitationDownloadLink, state.token, rawResponse: true) .then((response) { store.dispatch(StopLoading()); saveDownloadedFile(response.bodyBytes, diff --git a/lib/redux/purchase_order/purchase_order_actions.dart b/lib/redux/purchase_order/purchase_order_actions.dart index a5153c757..f5a4dddb4 100644 --- a/lib/redux/purchase_order/purchase_order_actions.dart +++ b/lib/redux/purchase_order/purchase_order_actions.dart @@ -829,7 +829,7 @@ void handlePurchaseOrderAction(BuildContext? context, break; case EntityAction.download: await WebClient() - .get(purchaseOrder.invitationEInvoiceDownloadLink, state.token, + .get(purchaseOrder.invitationDownloadLink, state.token, rawResponse: true) .then((response) { store.dispatch(StopLoading()); From 009a32e154271032623358dafaa77f213f0ada20 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 14 Nov 2023 09:15:31 +0200 Subject: [PATCH 133/138] Fix take picture --- lib/ui/app/document_grid.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/ui/app/document_grid.dart b/lib/ui/app/document_grid.dart index f1a3a2814..568fc1b6c 100644 --- a/lib/ui/app/document_grid.dart +++ b/lib/ui/app/document_grid.dart @@ -176,18 +176,18 @@ class _DocumentGridState extends State { final status = await Permission.camera.request(); if (status == PermissionStatus.granted) { final multipartFiles = []; - final images = await ImagePicker().pickMultiImage(); - for (var index = 0; index < images.length; index++) { - final image = images[index]; + final image = await ImagePicker() + .pickImage(source: ImageSource.camera); + if (image != null) { final croppedFile = (await ImageCropper() .cropImage(sourcePath: image.path))!; final bytes = await croppedFile.readAsBytes(); final multipartFile = MultipartFile.fromBytes( - 'documents[$index]', bytes, + 'documents[0]', bytes, filename: image.path.split('/').last); multipartFiles.add(multipartFile); + widget.onUploadDocument(multipartFiles, _isPrivate); } - widget.onUploadDocument(multipartFiles, _isPrivate); } else { openAppSettings(); } From af8c4ee55378c178fec5be1352f948994fbad6ab Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 14 Nov 2023 09:39:22 +0200 Subject: [PATCH 134/138] Fix for Android photos/storage permission --- android/app/src/debug/AndroidManifest.xml | 2 ++ android/app/src/main/AndroidManifest.foss.xml | 2 ++ android/app/src/main/AndroidManifest.xml | 2 ++ android/app/src/profile/AndroidManifest.xml | 2 ++ lib/utils/files.dart | 16 +++++++++---- macos/Flutter/GeneratedPluginRegistrant.swift | 2 ++ pubspec.foss.yaml | 1 + pubspec.lock | 24 +++++++++++++++++++ pubspec.yaml | 1 + 9 files changed, 48 insertions(+), 4 deletions(-) diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 3f3b5def9..d2698f76f 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -8,6 +8,7 @@ + @@ -15,6 +16,7 @@ + diff --git a/android/app/src/main/AndroidManifest.foss.xml b/android/app/src/main/AndroidManifest.foss.xml index a5333a944..abb2bb6a6 100644 --- a/android/app/src/main/AndroidManifest.foss.xml +++ b/android/app/src/main/AndroidManifest.foss.xml @@ -8,6 +8,7 @@ + @@ -15,6 +16,7 @@ + + @@ -15,6 +16,7 @@ + + @@ -15,6 +16,7 @@ + diff --git a/lib/utils/files.dart b/lib/utils/files.dart index 8ed69a243..3469a4a94 100644 --- a/lib/utils/files.dart +++ b/lib/utils/files.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:io' as file; // Flutter imports: +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; // Package imports: @@ -39,11 +40,18 @@ Future?> pickFiles({ allowMultiple: allowMultiple, ); } else { - final permission = await (fileType == FileType.image && Platform.isIOS - ? Permission.photos.request() - : Permission.storage.request()); + final androidInfo = await DeviceInfoPlugin().androidInfo; + PermissionStatus status; - if (permission == PermissionStatus.granted) { + if (Platform.isIOS && fileType == FileType.image) { + status = await Permission.photos.request(); + } else if (Platform.isAndroid && androidInfo.version.sdkInt >= 33) { + status = await Permission.photos.request(); + } else { + status = await Permission.storage.request(); + } + + if (status == PermissionStatus.granted) { return _pickFiles( fileIndex: fileIndex, fileType: fileType, diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 91ae4eeb9..99ae62b77 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import desktop_drop +import device_info_plus import file_selector_macos import in_app_purchase_storekit import in_app_review @@ -26,6 +27,7 @@ import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) InAppPurchasePlugin.register(with: registry.registrar(forPlugin: "InAppPurchasePlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index 9d7c93e06..e39c7650a 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -89,6 +89,7 @@ dependencies: # idb_shim: ^1.11.1+1 collection: ^1.15.0-nullsafety.4 filesystem_picker: ^4.0.0 + device_info_plus: ^9.1.0 dependency_overrides: intl: any diff --git a/pubspec.lock b/pubspec.lock index df00e5920..4069848de 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -306,6 +306,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.4" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "7035152271ff67b072a211152846e9f1259cf1be41e34cd3e0b5463d2d6b8419" + url: "https://pub.dev" + source: hosted + version: "9.1.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" + source: hosted + version: "7.0.0" diacritic: dependency: "direct main" description: @@ -1862,6 +1878,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.9" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" window_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 83853793a..c1b2b489c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -95,6 +95,7 @@ dependencies: # idb_shim: ^1.11.1+1 collection: ^1.15.0-nullsafety.4 filesystem_picker: ^4.0.0 + device_info_plus: ^9.1.0 dependency_overrides: intl: any From 7299dcd0ee30f5b91808d87f49c1a938e97594b8 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 14 Nov 2023 10:46:58 +0200 Subject: [PATCH 135/138] Update Sentry --- pubspec.foss.yaml | 2 +- pubspec.lock | 8 ++++---- pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index e39c7650a..c79ea0b9d 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -44,7 +44,7 @@ dependencies: git: url: https://github.com/theyakka/qr.flutter.git local_auth: ^2.1.5 - sentry_flutter: ^7.10.1 + sentry_flutter: ^7.12.0 image_picker: ^1.0.4 flutter_colorpicker: ^1.0.3 flutter_json_viewer: ^1.0.1 diff --git a/pubspec.lock b/pubspec.lock index 4069848de..624115ec5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1325,18 +1325,18 @@ packages: dependency: transitive description: name: sentry - sha256: "830667eadc0398fea3a3424ed1b74568e2db603a42758d0922e2f2974ce55a60" + sha256: "9cfd325611ab54b57d5e26957466823f05bea9d6cfcc8d48f11817b8bcedf0d1" url: "https://pub.dev" source: hosted - version: "7.10.1" + version: "7.12.0" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: "6730f41b304c6fb0fa590dacccaf73ba11082fc64b274cfe8a79776f2b95309c" + sha256: "0cd7d622cb63c94fd1b2f87ab508e158b950bd281e2a80f327ebf73bb217eaf3" url: "https://pub.dev" source: hosted - version: "7.10.1" + version: "7.12.0" share_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c1b2b489c..075f5316d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,7 +50,7 @@ dependencies: git: url: https://github.com/theyakka/qr.flutter.git local_auth: ^2.1.5 - sentry_flutter: ^7.10.1 + sentry_flutter: ^7.12.0 image_picker: ^1.0.4 flutter_colorpicker: ^1.0.3 flutter_json_viewer: ^1.0.1 From 416e9d9087284a50a6efec62d54e04597705ed93 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 14 Nov 2023 10:49:10 +0200 Subject: [PATCH 136/138] Update version --- .github/workflows/flatpak.yml | 2 +- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 1 + lib/constants.dart | 2 +- pubspec.foss.yaml | 2 +- pubspec.yaml | 2 +- snap/snapcraft.yaml | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 4752ebfca..f71b5dd43 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -86,7 +86,7 @@ jobs: draft: false prerelease: false title: "Latest Release" - automatic_release_tag: "v5.0.139" + automatic_release_tag: "v5.0.140" files: | ${{ github.workspace }}/artifacts/Invoice-Ninja-Archive ${{ github.workspace }}/artifacts/Invoice-Ninja-Hash diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index ba6fb785c..39a0ac36e 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -50,6 +50,7 @@ + diff --git a/lib/constants.dart b/lib/constants.dart index 5c8eb3aa5..4604c6c18 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -4,7 +4,7 @@ class Constants { } // TODO remove version once #46609 is fixed -const String kClientVersion = '5.0.139'; +const String kClientVersion = '5.0.140'; const String kMinServerVersion = '5.0.4'; const String kAppName = 'Invoice Ninja'; diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index c79ea0b9d..fc536eea5 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.139+139 +version: 5.0.140+140 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/pubspec.yaml b/pubspec.yaml index 075f5316d..869eb8cc8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.139+139 +version: 5.0.140+140 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 3726cf8bc..3b969622b 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: invoiceninja -version: '5.0.139' +version: '5.0.140' summary: Create invoices, accept payments, track expenses & time tasks description: "### Note: if the app fails to run using `snap run invoiceninja` it may help to run `/snap/invoiceninja/current/bin/invoiceninja` instead From e879c3c4b1a5b7b6b759264e4c3c782ce7db4276 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 14 Nov 2023 10:50:28 +0200 Subject: [PATCH 137/138] Force build --- lib/constants.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/constants.dart b/lib/constants.dart index 4604c6c18..0493d4047 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -8,7 +8,7 @@ const String kClientVersion = '5.0.140'; const String kMinServerVersion = '5.0.4'; const String kAppName = 'Invoice Ninja'; -const String kSiteUrl = 'https://invoiceninja.com'; +const String kSiteUrl = 'https://invoiceninja.com/'; const String kAppProductionUrl = 'https://invoicing.co'; const String kAppStagingUrl = 'https://staging.invoicing.co'; const String kAppStagingNetUrl = 'https://invoiceninja.net'; From 726297316309c992835fe895aa159f7fef8690f6 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 14 Nov 2023 11:12:39 +0200 Subject: [PATCH 138/138] Fix for sentry errors --- lib/constants.dart | 2 +- lib/ui/app/live_text.dart | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/constants.dart b/lib/constants.dart index 0493d4047..4604c6c18 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -8,7 +8,7 @@ const String kClientVersion = '5.0.140'; const String kMinServerVersion = '5.0.4'; const String kAppName = 'Invoice Ninja'; -const String kSiteUrl = 'https://invoiceninja.com/'; +const String kSiteUrl = 'https://invoiceninja.com'; const String kAppProductionUrl = 'https://invoicing.co'; const String kAppStagingUrl = 'https://staging.invoicing.co'; const String kAppStagingNetUrl = 'https://invoiceninja.net'; diff --git a/lib/ui/app/live_text.dart b/lib/ui/app/live_text.dart index 77acc2816..690734734 100644 --- a/lib/ui/app/live_text.dart +++ b/lib/ui/app/live_text.dart @@ -3,6 +3,7 @@ import 'dart:async'; // Flutter imports: import 'package:flutter/widgets.dart'; +import 'package:invoiceninja_flutter/utils/platforms.dart'; import 'package:window_manager/window_manager.dart'; class LiveText extends StatefulWidget { @@ -29,11 +30,13 @@ class _LiveTextState extends State { void initState() { super.initState(); _timer = Timer.periodic( - widget.duration ?? Duration(milliseconds: 100), + widget.duration ?? Duration(milliseconds: 500), (Timer timer) async { - final isFocused = await windowManager.isFocused(); - if (!isFocused) { - return; + if (isDesktopOS()) { + final isFocused = await windowManager.isFocused(); + if (!isFocused) { + return; + } } if (mounted) {