diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f26f3d566..09725687f 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 @@ -19,19 +24,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@v2 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 +50,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) @@ -60,7 +72,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 @@ -70,6 +82,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 +96,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 diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml new file mode 100644 index 000000000..f71b5dd43 --- /dev/null +++ b/.github/workflows/flatpak.yml @@ -0,0 +1,115 @@ +name: Build Flatpak + +on: + workflow_dispatch: + #push: + # branches: + # - master + +#concurrency: +# group: ci-release-${{ github.ref }}-1 +# cancel-in-progress: true + +env: + project-id: com.invoiceninja.InvoiceNinja + +jobs: + build-flutter-app: + name: Build Flutter + env: + api_secret: ${{ secrets.api_secret }} + commit_secret: ${{ secrets.commit_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 + run: | + archiveName=Invoice-Ninja-Linux-Portable.tar.gz + baseDir=$(pwd) + + flutter build linux + + cd build/linux/x64/release/bundle || exit + tar -czaf $archiveName ./* + shasum -a 256 $archiveName > Hashes.txt + + mv $archiveName "$baseDir"/ + mv Hashes.txt "$baseDir"/ + + - name: Upload app archive to workflow + uses: actions/upload-artifact@v3 + with: + 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: Hashes.txt + + - name: Download artifacts + uses: actions/download-artifact@v3 + with: + path: artifacts + + - name: Create Release + uses: marvinpinto/action-automatic-releases@v1.2.1 + with: + repo_token: "${{ secrets.commit_secret }}" + draft: false + prerelease: false + title: "Latest Release" + automatic_release_tag: "v5.0.140" + files: | + ${{ github.workspace }}/artifacts/Invoice-Ninja-Archive + ${{ github.workspace }}/artifacts/Invoice-Ninja-Hash + +# 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/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/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 } diff --git a/assets/images/com.invoiceninja.InvoiceNinja.svg b/assets/images/com.invoiceninja.InvoiceNinja.svg new file mode 100644 index 000000000..8e838c8f8 --- /dev/null +++ b/assets/images/com.invoiceninja.InvoiceNinja.svg @@ -0,0 +1,2 @@ + +Invoice Ninja icon \ No newline at end of file diff --git a/bump_version.sh b/bump_version.sh new file mode 100644 index 000000000..d2e49108a --- /dev/null +++ b/bump_version.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +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/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.InvoiceNinja.desktop b/flatpak/com.invoiceninja.InvoiceNinja.desktop new file mode 100644 index 000000000..a0928a4d5 --- /dev/null +++ b/flatpak/com.invoiceninja.InvoiceNinja.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Version=1.0 +Type=Application + +Name=Invoice Ninja +Comment=Create invoices, accept payments, track expenses & time tasks +Categories=Productivity; + +Icon=com.invoiceninja.InvoiceNinja +Exec=invoiceninja_client +Terminal=false +StartupWMClass=invoiceninja_client \ No newline at end of file diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml new file mode 100644 index 000000000..39a0ac36e --- /dev/null +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -0,0 +1,59 @@ + + + + com.invoiceninja.InvoiceNinja + Invoice Ninja + 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 + + pointing + keyboard + touch + + +

Create. Send. 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. +

+

+ • 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.InvoiceNinja.desktop + + + https://raw.githubusercontent.com/invoiceninja/admin-portal/master/samples/screenshots/5.png + + + + + + + + + + +
\ No newline at end of file diff --git a/lib/constants.dart b/lib/constants.dart index 318dcc1ad..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.127'; +const String kClientVersion = '5.0.140'; const String kMinServerVersion = '5.0.4'; const String kAppName = 'Invoice Ninja'; @@ -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'; @@ -736,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/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/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/data/models/design_model.dart b/lib/data/models/design_model.dart index 3bec1ac53..d6d8607cc 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 @@ -224,9 +231,14 @@ 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; + 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/data/models/entities.dart b/lib/data/models/entities.dart index c047c1349..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 [ @@ -285,6 +286,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 +295,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/invoice_model.dart b/lib/data/models/invoice_model.dart index 759eeca90..b8e2f4246 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, ); @@ -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; @@ -635,6 +632,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 { @@ -1552,7 +1564,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 +1578,7 @@ abstract class InvoiceItemEntity taxRate2: 0, taxName3: '', taxRate3: 0, - typeId: TYPE_STANDARD, + typeId: typeId ?? TYPE_STANDARD, customValue1: '', customValue2: '', customValue3: '', @@ -1781,6 +1794,7 @@ abstract class InvitationEntity extends Object sentDate: '', viewedDate: '', openedDate: '', + messageId: '', updatedAt: 0, archivedAt: 0, isDeleted: false, @@ -1822,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}'; @@ -1890,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 8e643f054..bc63bedab 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 [ @@ -994,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)), @@ -1093,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?; @@ -1570,8 +1566,6 @@ class _$InvoiceEntity extends InvoiceEntity { @override final bool autoBillEnabled; @override - final String? filename; - @override final BuiltList? recurringDates; @override final BuiltList lineItems; @@ -1670,7 +1664,6 @@ class _$InvoiceEntity extends InvoiceEntity { this.invoiceId, this.recurringId, required this.autoBillEnabled, - this.filename, this.recurringDates, required this.lineItems, required this.invitations, @@ -1867,7 +1860,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 +1941,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 +2022,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 +2296,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 +2448,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 +2556,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(), @@ -3009,6 +2993,8 @@ class _$InvitationEntity extends InvitationEntity { @override final String emailError; @override + final String messageId; + @override final bool? isChanged; @override final int createdAt; @@ -3041,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, @@ -3067,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( @@ -3137,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) @@ -3192,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; @@ -3246,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; @@ -3295,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'), diff --git a/lib/data/models/settings_model.dart b/lib/data/models/settings_model.dart index 00a5de4f5..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; @@ -884,6 +896,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 +931,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/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/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/data/models/vendor_model.dart b/lib/data/models/vendor_model.dart index 71258a796..6a8942c12 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 : calculateDisplayName; } @override @@ -520,6 +525,7 @@ abstract class VendorEntity extends Object ..activities.replace(BuiltList()) ..lastLogin = 0 ..languageId = '' + ..displayName = '' ..classification = ''; static Serializer get serializer => _$vendorEntitySerializer; @@ -549,6 +555,7 @@ abstract class VendorContactEntity extends Object customValue3: '', customValue4: '', link: '', + password: '', ); } @@ -579,6 +586,8 @@ abstract class VendorContactEntity extends Object String get phone; + String get password; + @BuiltValueField(wireName: 'custom_value1') String get customValue1; @@ -607,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; @@ -659,6 +676,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 f79da094c..fe79c68f0 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; @@ -434,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)), @@ -528,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; @@ -790,6 +804,8 @@ class _$VendorEntity extends VendorEntity { @override final String name; @override + final String displayName; + @override final String address1; @override final String address2; @@ -860,6 +876,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 +911,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 +977,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 +1018,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 +1060,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 +1109,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 +1259,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 +1319,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 +1333,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'), @@ -1367,6 +1394,8 @@ class _$VendorContactEntity extends VendorContactEntity { @override final String phone; @override + final String password; + @override final String customValue1; @override final String customValue2; @@ -1404,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, @@ -1430,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( @@ -1467,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 && @@ -1493,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); @@ -1519,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) @@ -1564,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; @@ -1631,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; @@ -1678,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/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..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); @@ -68,7 +68,7 @@ class WebClient { String url, String? token, { dynamic data, - List? multipartFiles, + List? multipartFiles, String? secret, String? password, String? idToken, @@ -113,12 +113,12 @@ class WebClient { client.close(); } + _checkResponse(url, response); + if (rawResponse) { return response; } - _checkResponse(url, response); - return json.decode(response.body); } @@ -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'; @@ -287,7 +293,7 @@ String _parseError(int code, String response) { try { final dynamic jsonResponse = json.decode(response); - message = jsonResponse['message'] ?? jsonResponse; + message += jsonResponse['message'] ?? jsonResponse; if (jsonResponse['errors'] != null && (jsonResponse['errors'] as Map).isNotEmpty) { @@ -309,12 +315,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/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(); -} 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/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/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/credit/credit_actions.dart b/lib/redux/credit/credit_actions.dart index 620e5b7b7..83d1b4599 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,16 @@ Future handleCreditAction(BuildContext context, List credits, ); break; case EntityAction.download: - launchUrl(Uri.parse(credit.invitationDownloadLink)); + store.dispatch(StartLoading()); + await WebClient() + .get(credit.invitationDownloadLink, 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( @@ -678,7 +688,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/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/design/design_selectors.dart b/lib/redux/design/design_selectors.dart index 976235648..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: @@ -98,3 +99,20 @@ String? getDesignIdForVendorByEntity( return settings.defaultInvoiceDesignId; } } + +bool hasDesignTemplatesForEntityType( + BuiltMap designMap, EntityType entityType) { + if (!kReleaseMode) { + return true; + } + + var hasMatch = false; + + designMap.forEach((designId, design) { + if (design.supportsEntityType(entityType)) { + hasMatch = true; + } + }); + + return hasMatch; +} diff --git a/lib/redux/document/document_actions.dart b/lib/redux/document/document_actions.dart index 70e3ca54c..9b81551f8 100644 --- a/lib/redux/document/document_actions.dart +++ b/lib/redux/document/document_actions.dart @@ -1,16 +1,12 @@ // Dart imports: import 'dart:async'; -import 'dart:io' as file; -import 'dart:io'; // Flutter imports: import 'package:built_collection/built_collection.dart'; -import 'package:flutter/foundation.dart'; 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,14 +32,9 @@ 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({ this.force = false, @@ -435,33 +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 { - 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) { store.dispatch(LoadDocumentData( diff --git a/lib/redux/invoice/invoice_actions.dart b/lib/redux/invoice/invoice_actions.dart index 9c02e441e..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( @@ -824,7 +844,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/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/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/purchase_order/purchase_order_actions.dart b/lib/redux/purchase_order/purchase_order_actions.dart index cfd5b4d03..f5a4dddb4 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'; @@ -625,7 +626,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; @@ -827,7 +828,17 @@ void handlePurchaseOrderAction(BuildContext? context, .recreateInvitations(state)); break; case EntityAction.download: - launchUrl(Uri.parse(purchaseOrder.invitationDownloadLink)); + await WebClient() + .get(purchaseOrder.invitationDownloadLink, 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/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/quote/quote_actions.dart b/lib/redux/quote/quote_actions.dart index c13551211..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( @@ -731,7 +741,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/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/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); 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/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/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/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), 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( 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(); } diff --git a/lib/ui/app/forms/app_dropdown_button.dart b/lib/ui/app/forms/app_dropdown_button.dart index ebae2f6f3..ebb0d7623 100644 --- a/lib/ui/app/forms/app_dropdown_button.dart +++ b/lib/ui/app/forms/app_dropdown_button.dart @@ -34,33 +34,23 @@ 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 - ], - ), - ); - - if (labelText != null) { - dropDownButton = InputDecorator( - decoration: InputDecoration( - labelText: labelText!.isEmpty ? null : labelText, + return 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!), ), - isEmpty: isEmpty && blankLabel == null, - child: dropDownButton); - } - - return dropDownButton; + ...items + ], + ); } } 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!, diff --git a/lib/ui/app/forms/design_picker.dart b/lib/ui/app/forms/design_picker.dart index cf7c5ecb7..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'; @@ -15,11 +16,15 @@ class DesignPicker extends StatelessWidget { required this.onSelected, this.label, this.initialValue, + this.showBlank = false, + this.entityType, }); - final Function(DesignEntity) onSelected; + final Function(DesignEntity?) onSelected; final String? label; final String? initialValue; + final bool showBlank; + final EntityType? entityType; @override Widget build(BuildContext context) { @@ -29,18 +34,22 @@ class DesignPicker extends StatelessWidget { final designState = state.designState; 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]; + 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, 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) { 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/ui/app/variables.dart b/lib/ui/app/variables.dart index 10f5bcaca..6bc24d4a2 100644 --- a/lib/ui/app/variables.dart +++ b/lib/ui/app/variables.dart @@ -184,6 +184,8 @@ class _VariablesHelpState extends State CompanyFields.country, CompanyFields.address1, CompanyFields.address2, + CompanyFields.city, + CompanyFields.postalCode, CompanyFields.idNumber, CompanyFields.email, CompanyFields.phone, diff --git a/lib/ui/client/client_pdf.dart b/lib/ui/client/client_pdf.dart index 01971eda9..c32a1c301 100644 --- a/lib/ui/client/client_pdf.dart +++ b/lib/ui/client/client_pdf.dart @@ -1,11 +1,9 @@ // Dart imports: import 'dart:async'; import 'dart:convert'; -import 'dart:io' as file; // Flutter imports: import 'package:built_collection/built_collection.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; // Package imports: @@ -16,11 +14,14 @@ 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'; 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'; import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:printing/printing.dart'; @@ -39,10 +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 'package:share_plus/share_plus.dart'; - class ClientPdfView extends StatefulWidget { const ClientPdfView({ Key? key, @@ -68,15 +65,7 @@ class _ClientPdfViewState extends State { convertDateTimeToSqlDate(DateTime.now().subtract(Duration(days: 365))); String? _endDate = convertDateTimeToSqlDate(); String _status = kStatementStatusAll; - - @override - void initState() { - super.initState(); - - //final state = widget.viewModel.state; - //final settings = state.dashboardUIState.settings; - //_dateRange = settings.dateRange; - } + String? _designId; @override void didChangeDependencies() { @@ -121,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; @@ -133,6 +123,9 @@ class _ClientPdfViewState extends State { if (sendEmail) { url += '?send_email=true'; } + if (designId.isNotEmpty) { + url += '&design_id=$designId'; + } String? startDate = ''; String? endDate = ''; @@ -171,19 +164,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; } @@ -191,8 +171,97 @@ 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 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, + //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 = Expanded( + 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 = Expanded( + 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: '', + height: 50, + )); /* final pageSelector = _pageCount == 1 @@ -237,104 +306,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: [ @@ -346,38 +322,9 @@ class _ClientPdfViewState extends State { : () async { final fileName = localization.statement + '_' + - (client!.number) + + (client.number) + '.pdf'; - if (kIsWeb) { - 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); }, ), AppTextButton( @@ -386,7 +333,7 @@ class _ClientPdfViewState extends State { onPressed: _response == null ? null : () async { - if (!client!.hasEmailAddress) { + if (!client.hasEmailAddress) { showMessageDialog( message: localization.clientEmailNotSet, secondaryActions: [ @@ -432,7 +379,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 = @@ -449,59 +396,102 @@ class _ClientPdfViewState extends State { child: Text(localization.close, style: TextStyle(color: state.headerTextColor)), onPressed: () { - viewEntity(entity: client!); + viewEntity(entity: client); }, ), ], ) : null, body: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (_dateRange == DateRange.custom) - Container( - width: double.infinity, - color: Theme.of(context).colorScheme.background, - child: Wrap( - alignment: WrapAlignment.center, + Material( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - width: 180, - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: DatePicker( - labelText: localization!.startDate, - onSelected: (value, _) { - setState(() { - _startDate = value; - }); - }, - selectedDate: _startDate, + 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( + 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(), + ), + ) + ], ), - ), - 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() @@ -510,9 +500,9 @@ class _ClientPdfViewState extends State { canChangeOrientation: false, canChangePageFormat: false, canDebug: false, - maxPageWidth: 800, + maxPageWidth: 600, pdfFileName: - localization!.statement + '_' + client!.number + '.pdf', + 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/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), ], ), ), diff --git a/lib/ui/design/edit/design_edit.dart b/lib/ui/design/edit/design_edit.dart index d69eb060e..dfd8900f0 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'; @@ -425,30 +426,15 @@ 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)!; + final state = widget.viewModel.state; + final design = widget.viewModel.design; + final entityTypes = design.entities.split(','); return ScrollableListView( + primary: true, children: [ FormCard( children: [ @@ -460,15 +446,17 @@ 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: 20), // 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 +464,51 @@ 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, + 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) { + final entities = entityTypes; + if (value == true) { + entities.add(entityType.apiValue); + } else { + entities.remove(entityType.apiValue); + } + widget.viewModel.onChanged(design.rebuild((b) => b + ..entities = entities + .where((entity) => entity.isNotEmpty) + .join(','))); + }, + title: Text(localization.lookup(entityType.plural)), + controlAffinity: ListTileControlAffinity.leading, + )) + .toList(), + ], ], ), Padding( @@ -651,7 +683,7 @@ class _PdfDesignPreviewState extends State { allowPrinting: false, allowSharing: false, canDebug: false, - maxPageWidth: 800, + maxPageWidth: 600, ) else SizedBox(), 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, ); } } diff --git a/lib/ui/invoice/edit/invoice_edit_contacts.dart b/lib/ui/invoice/edit/invoice_edit_contacts.dart index 647baca97..05af4e832 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, @@ -182,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( @@ -277,7 +278,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( diff --git a/lib/ui/invoice/edit/invoice_edit_desktop.dart b/lib/ui/invoice/edit/invoice_edit_desktop.dart index ae2ac2da1..6ac976ffe 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), @@ -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( @@ -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_details.dart b/lib/ui/invoice/edit/invoice_edit_details.dart index ba6d8aac2..2c0930b3e 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), @@ -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/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_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/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); } diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index 89c3ff9b0..a33cf2fe8 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -1,7 +1,5 @@ // Dart imports: import 'dart:async'; -import 'dart:convert'; -import 'dart:io' as file; // Flutter imports: import 'package:flutter/foundation.dart'; @@ -9,15 +7,14 @@ 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/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'; import 'package:printing/printing.dart'; -import 'package:share_plus/share_plus.dart'; -import 'package:url_launcher/url_launcher.dart'; // Project imports: import 'package:invoiceninja_flutter/data/models/models.dart'; @@ -29,14 +26,10 @@ 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'; -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, @@ -54,8 +47,8 @@ class InvoicePdfView extends StatefulWidget { class _InvoicePdfViewState extends State { bool _isLoading = true; bool _isDeliveryNote = false; + String? _designId; String? _activityId; - String? _pdfString; http.Response? _response; //int _pageCount = 1; //int _currentPage = 1; @@ -70,13 +63,13 @@ class _InvoicePdfViewState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); + loadPdf(); } void loadPdf() { final viewModel = widget.viewModel; final invoice = viewModel.invoice!; - final state = viewModel.state; if (invoice.invitations.isEmpty) { return; @@ -91,16 +84,11 @@ class _InvoicePdfViewState extends State { invoice, _isDeliveryNote, _activityId, + _designId, ).then((response) async { 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,66 +132,77 @@ 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.history - .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 || + !hasDesignTemplatesForEntityType( + state.designState.map, invoice.entityType!) + ? SizedBox() + : Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 17), + 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 = 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 = Flexible( + child: CheckboxListTile( + title: Text( + localization.deliveryNote, ), + value: _isDeliveryNote, + onChanged: (value) { + setState(() { + _isDeliveryNote = !_isDeliveryNote; + loadPdf(); + }); + }, + controlAffinity: ListTileControlAffinity.leading, + activeColor: state.accentColor, ), ); @@ -217,116 +216,84 @@ 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)); - } else { - final fileName = localization - .lookup('${invoice.entityType}') + + backgroundColor: Colors.grey.shade300, + appBar: widget.showAppBar + ? AppBar( + centerTitle: false, + automaticallyImplyLeading: isMobile(context), + title: + Text(EntityPresenter().initialize(invoice, context).title()!), + 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 { + final fileName = + localization.lookup('${invoice.entityType}') + '_' + (invoice.number.isEmpty ? localization.pending : invoice.number) + '.pdf'; - if (kIsWeb) { - 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)]); - } - } - } - }, - ), - 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!) + saveDownloadedFile(_response!.bodyBytes, fileName); + }, + ), + if (isDesktop(context)) + TextButton( + child: Text(localization.close, + style: TextStyle(color: state.headerTextColor)), + onPressed: () { + viewEntity(entity: invoice); + }, + ), + ], + ) + : null, + body: Column( + children: [ + 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() : PdfPreview( build: (format) => _response!.bodyBytes, canChangeOrientation: false, canChangePageFormat: false, canDebug: false, - maxPageWidth: 800, + maxPageWidth: 600, pdfFileName: localization.lookup(invoice.entityType!.snakeCase) + '_' + invoice.number + '.pdf', - )); + ), + ), + ], + ), + ); } } @@ -335,36 +302,28 @@ Future _loadPDF( InvoiceEntity invoice, bool isDeliveryNote, String? activityId, + String? designId, ) async { - http.Response? response; + final store = StoreProvider.of(context); + final state = store.state; + final credentials = store.state.credentials; + final invitation = invoice.invitations.first; - if ((activityId ?? '').isNotEmpty || isDeliveryNote) { - final store = StoreProvider.of(context); - final credential = store.state.credentials; - final url = isDeliveryNote - ? '/invoices/${invoice.id}/delivery_note' - : '/activities/download_entity/$activityId'; - response = await WebClient() - .get('${credential.url}$url', credential.token, rawResponse: true); + var url = ''; + + if ((activityId ?? '').isNotEmpty) { + url = '${credentials.url}/activities/download_entity/$activityId'; } else { - final invitation = invoice.invitations.first; - final url = invitation.downloadLink; - 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; + if (isDeliveryNote) { + url = '${credentials.url}/invoices/${invoice.id}/delivery_note'; + } else { + url = invitation.downloadLink; } - showErrorDialog(message: errorMessage); - throw errorMessage; + if ((designId ?? '').isNotEmpty) { + url += '&design_id=$designId'; + } } - return response; + return await WebClient().get(url, state.token, rawResponse: true); } 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), ), ), diff --git a/lib/ui/invoice/view/invoice_view_history.dart b/lib/ui/invoice/view/invoice_view_history.dart index dd19c762d..bdf94270c 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: @@ -37,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) { @@ -44,12 +46,18 @@ class _InvoiceViewHistoryState extends State { } final activityList = invoice.activities - .where((activity) => activity.history != null) + .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)); if (activityList.isEmpty) { - return HelpText(AppLocalization.of(context)!.noHistory); + return HelpText(localization.noHistory); } return ScrollableListViewBuilder( @@ -64,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) { @@ -76,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)! + @@ -97,7 +109,7 @@ class _InvoiceViewHistoryState extends State { ); }, separatorBuilder: (context, index) => ListDivider(), - itemCount: invoice.history.length, + itemCount: activityList.length, ); } } 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/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; } 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( 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/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/reports_screen.dart b/lib/ui/reports/reports_screen.dart index a30bf0478..15877ebd8 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)) @@ -187,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, @@ -1390,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)); diff --git a/lib/ui/reports/reports_screen_vm.dart b/lib/ui/reports/reports_screen_vm.dart index 8df0d9bef..58ef63657 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, @@ -637,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/ui/reports/task_item_report.dart b/lib/ui/reports/task_item_report.dart new file mode 100644 index 000000000..1d5236912 --- /dev/null +++ b/lib/ui/reports/task_item_report.dart @@ -0,0 +1,315 @@ +// 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, + is_invoiced, +} + +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); + break; + case TaskItemReportFields.is_invoiced: + value = task.isInvoiced; + break; + } + + 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/reports/task_report.dart b/lib/ui/reports/task_report.dart index 3e3344af1..e915a39ff 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,8 @@ enum TaskReportFields { assigned_to, created_by, amount, + record_state, + is_invoiced, } var memoizedTaskReport = memo10(( @@ -247,6 +251,13 @@ ReportResult taskReport( )!, ); break; + case TaskReportFields.record_state: + value = AppLocalization.of(navigatorKey.currentContext!)! + .lookup(task.entityState); + break; + case TaskReportFields.is_invoiced: + value = task.isInvoiced; + break; } 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( 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/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/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/invoice_design.dart b/lib/ui/settings/invoice_design.dart index 06e5e6710..84491515d 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,57 @@ class _InvoiceDesignState extends State ), SizedBox(height: 16), ], + 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( + children: [ AppDropdownButton( labelText: localization.pageLayout, value: settings.pageLayout, @@ -1328,7 +1380,7 @@ class _PdfPreviewState extends State<_PdfPreview> { canChangeOrientation: false, canChangePageFormat: false, canDebug: false, - maxPageWidth: 800, + maxPageWidth: 600, allowPrinting: false, allowSharing: false, ), diff --git a/lib/ui/settings/settings_list.dart b/lib/ui/settings/settings_list.dart index accd23cf8..b88c5364e 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,6 +506,7 @@ class SettingsSearch extends StatelessWidget { 'show_pdf_preview', 'pdf_preview_location#2022-10-24', 'refresh_data', + if (!kIsWeb) 'downloads_folder#2023-10-29' ], [ 'dark_mode', @@ -549,7 +551,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#2023-11-06', + 'statement_design#2023-11-06', + 'payment_receipt_design#2023-11-06', + //'payment_refund_design#2023-11-06', + ], ], ], kSettingsCustomDesigns: [ @@ -601,10 +609,8 @@ class SettingsSearch extends StatelessWidget { [ 'header', 'footer', - if (isSelfHosted(context)) ...[ - 'custom_css', - 'custom_javascript', - ], + 'custom_css', + if (isSelfHosted(context)) 'custom_javascript', ], ], kSettingsEmailSettings: [ 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); 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/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(), 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 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/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); diff --git a/lib/utils/files.dart b/lib/utils/files.dart index 0e8927df6..3469a4a94 100644 --- a/lib/utils/files.dart +++ b/lib/utils/files.dart @@ -1,13 +1,18 @@ // Dart imports: 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: 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'; import 'package:invoiceninja_flutter/utils/dialogs.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:path_provider/path_provider.dart'; @@ -19,6 +24,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, @@ -34,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, @@ -82,23 +95,62 @@ Future?> _pickFiles({ return null; } -Future getAppDownloadDirectory() async { - final directory = await (isDesktopOS() - ? getDownloadsDirectory() - : getApplicationDocumentsDirectory()); +void saveDownloadedFile(Uint8List data, String fileName) async { + if (kIsWeb) { + WebUtils.downloadBinaryFile(fileName, data); + } else { + final directory = await getAppDownloadDirectory(); + if (directory != null) { + String filePath = '$directory/${file.Platform.pathSeparator}$fileName'; - if (directory == null) { - return null; + 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 = ''; + + 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)); + .downloadsFolderDoesNotExist + .replaceFirst(':value', path)); return null; } - return directory.path; + return path; } 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]; diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index 1fbff80cd..f88f95f22 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -18,10 +18,22 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment -'total_invoiced_quotes': 'Invoiced Quotes', + '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', + 'payment_refund_design': 'Payment Refund Design', + 'quarter': 'Quarter', + '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', + '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 +109914,63 @@ 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']!; + + String get lastLogin => + _localizedValues[localeCode]!['last_login'] ?? + _localizedValues['en']!['last_login']!; + + String get recordState => + _localizedValues[localeCode]!['record_state'] ?? + _localizedValues['en']!['record_state']!; + + String get taskItem => + _localizedValues[localeCode]!['task_item'] ?? + _localizedValues['en']!['task_item']!; + + String get quarter => + _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']!; + + String get templateHelp => + _localizedValues[localeCode]!['template_help'] ?? + _localizedValues['en']!['template_help']!; + + // STARTER: lang field - do not remove comment String lookup(String? key) { final lookupKey = toSnakeCase(key); 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/oauth.dart.foss b/lib/utils/oauth.dart.foss index cc3c38e51..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 void signOut() async { + static Future signOut() async { // } - static void disconnect() async { + static Future disconnect() async { // } -} +} \ No newline at end of file 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; 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. /// 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 4ffdf9dae..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.127+127 +version: 5.0.140+140 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none @@ -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 @@ -88,6 +88,8 @@ dependencies: # quick_actions: ^0.2.1 # 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 0044dfe05..624115ec5 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: @@ -386,6 +402,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: @@ -536,22 +560,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: @@ -580,10 +596,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: @@ -1309,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: @@ -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 530fd9779..869eb8cc8 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.140+140 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none @@ -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 @@ -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 @@ -94,6 +94,8 @@ dependencies: # quick_actions: ^0.2.1 # 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/samples/screenshots/5.png b/samples/screenshots/5.png new file mode 100644 index 000000000..a89894828 Binary files /dev/null and b/samples/screenshots/5.png differ diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 35bbce8a1..3b969622b 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,11 +1,11 @@ name: invoiceninja -version: '5.0.127' -summary: Create invoices, accept payments, track expenses & time-tasks +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 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.