From 102c5d36681fbb88b54c8b11b79ebb93888d76c8 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Fri, 30 Jan 2026 16:54:56 +0100 Subject: [PATCH] front dev update --- .gitignore | 15 +- .../lib/api/requests/ledger/create.dart | 5 +- .../lib/api/responses/cursor_page.dart | 19 ++ .../lib/api/responses/payment/payments.dart | 11 +- frontend/pshared/lib/config/common.dart | 4 - frontend/pshared/lib/config/common_dev.dart | 49 +++++ .../pshared/lib/data/dto/ledger/account.dart | 6 +- .../pshared/lib/data/dto/ledger/role.dart | 107 +++++++++++ .../lib/data/mapper/ledger/account.dart | 5 +- .../pshared/lib/data/mapper/ledger/role.dart | 65 +++++++ .../lib/data/mapper/payment/enums.dart | 4 + frontend/pshared/lib/l10n/en.arb | 5 + frontend/pshared/lib/l10n/ru.arb | 5 + .../pshared/lib/models/ledger/account.dart | 7 +- frontend/pshared/lib/models/ledger/role.dart | 14 ++ .../lib/models/pagination/cursor_page.dart | 6 + .../lib/models/payment/chain_network.dart | 3 +- frontend/pshared/lib/models/payment/page.dart | 5 + frontend/pshared/lib/provider/ledger.dart | 3 +- .../lib/provider/payment/payments.dart | 181 ++++++++++++++++++ .../lib/provider/payment/provider.dart | 7 +- .../payment/quotation/intent_builder.dart | 6 + .../pshared/lib/provider/permissions.dart | 6 +- frontend/pshared/lib/service/ledger.dart | 4 +- .../pshared/lib/service/payment/service.dart | 44 +++-- frontend/pshared/lib/utils/http/params.dart | 37 ++++ frontend/pshared/lib/utils/l10n/chain.dart | 2 + frontend/pweb/lib/main.dart | 10 +- frontend/pweb/lib/pages/report/page.dart | 167 ++++++++++++++-- .../pweb/lib/pages/report/table/filters.dart | 15 +- frontend/pweb/lib/pages/report/table/row.dart | 12 +- 31 files changed, 755 insertions(+), 74 deletions(-) create mode 100644 frontend/pshared/lib/api/responses/cursor_page.dart create mode 100644 frontend/pshared/lib/config/common_dev.dart create mode 100644 frontend/pshared/lib/data/dto/ledger/role.dart create mode 100644 frontend/pshared/lib/data/mapper/ledger/role.dart create mode 100644 frontend/pshared/lib/models/ledger/role.dart create mode 100644 frontend/pshared/lib/models/pagination/cursor_page.dart create mode 100644 frontend/pshared/lib/models/payment/page.dart create mode 100644 frontend/pshared/lib/provider/payment/payments.dart diff --git a/.gitignore b/.gitignore index df1e1b4f..5c9ac87c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,19 @@ devtools_options.yaml untranslated.txt generate_protos.sh update_dep.sh +test.sh .vscode/ .gocache/ -.cache/ \ No newline at end of file +.cache/ +.claude/ + +# Air hot reload build artifacts +**/tmp/ +build-errors.log + +# Development environment (NEVER commit credentials!) +.env.dev +vault-keys.txt +.vault_token + +CLAUDE.md \ No newline at end of file diff --git a/frontend/pshared/lib/api/requests/ledger/create.dart b/frontend/pshared/lib/api/requests/ledger/create.dart index ac32bf67..dc1ae3c9 100644 --- a/frontend/pshared/lib/api/requests/ledger/create.dart +++ b/frontend/pshared/lib/api/requests/ledger/create.dart @@ -1,6 +1,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:pshared/data/dto/describable.dart'; +import 'package:pshared/data/dto/ledger/role.dart'; import 'package:pshared/data/dto/ledger/type.dart'; part 'create.g.dart'; @@ -11,7 +12,7 @@ class CreateLedgerAccountRequest { final Map? metadata; final String currency; final bool allowNegative; - final bool isSettlement; + final LedgerAccountRoleDTO role; final DescribableDTO describable; final String? ownerRef; final LedgerAccountTypeDTO accountType; @@ -20,7 +21,7 @@ class CreateLedgerAccountRequest { this.metadata, required this.currency, required this.allowNegative, - required this.isSettlement, + required this.role, required this.describable, required this.accountType, this.ownerRef, diff --git a/frontend/pshared/lib/api/responses/cursor_page.dart b/frontend/pshared/lib/api/responses/cursor_page.dart new file mode 100644 index 00000000..8d84b5ad --- /dev/null +++ b/frontend/pshared/lib/api/responses/cursor_page.dart @@ -0,0 +1,19 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/api/responses/base.dart'; +import 'package:pshared/api/responses/token.dart'; + +part 'cursor_page.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class CursorPageResponse extends BaseAuthorizedResponse { + @JsonKey(name: 'next_cursor') + final String? nextCursor; + + const CursorPageResponse({required super.accessToken, required this.nextCursor}); + + factory CursorPageResponse.fromJson(Map json) => _$CursorPageResponseFromJson(json); + @override + Map toJson() => _$CursorPageResponseToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/api/responses/payment/payments.dart b/frontend/pshared/lib/api/responses/payment/payments.dart index 42098273..27eba2dc 100644 --- a/frontend/pshared/lib/api/responses/payment/payments.dart +++ b/frontend/pshared/lib/api/responses/payment/payments.dart @@ -1,6 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:pshared/api/responses/base.dart'; +import 'package:pshared/api/responses/cursor_page.dart'; import 'package:pshared/api/responses/token.dart'; import 'package:pshared/data/dto/payment/payment.dart'; @@ -8,11 +8,14 @@ part 'payments.g.dart'; @JsonSerializable(explicitToJson: true) -class PaymentsResponse extends BaseAuthorizedResponse { - +class PaymentsResponse extends CursorPageResponse { final List payments; - const PaymentsResponse({required super.accessToken, required this.payments}); + const PaymentsResponse({ + required super.accessToken, + required super.nextCursor, + required this.payments, + }); factory PaymentsResponse.fromJson(Map json) => _$PaymentsResponseFromJson(json); @override diff --git a/frontend/pshared/lib/config/common.dart b/frontend/pshared/lib/config/common.dart index 51641dbe..71a9cd3f 100644 --- a/frontend/pshared/lib/config/common.dart +++ b/frontend/pshared/lib/config/common.dart @@ -4,10 +4,6 @@ import 'package:flutter/material.dart'; class CommonConstants { static String apiProto = 'https'; static String apiHost = 'app.sendico.io'; - // static String apiProto = const String.fromEnvironment('API_PROTO', defaultValue: 'http'); - // static String apiHost = const String.fromEnvironment('API_HOST', defaultValue: 'localhost'); - // static String apiHost = 'localhost'; - // static String apiHost = '10.0.2.2'; static String apiEndpoint = '/api/v1'; static String amplitudeSecret = 'c3d75b3e2520d708440acbb16b923e79'; static String amplitudeServerZone = 'EU'; diff --git a/frontend/pshared/lib/config/common_dev.dart b/frontend/pshared/lib/config/common_dev.dart new file mode 100644 index 00000000..139f6b6c --- /dev/null +++ b/frontend/pshared/lib/config/common_dev.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + + +class CommonConstants { + static String apiProto = 'http'; + static String apiHost = 'localhost:8080'; + static String apiEndpoint = '/api/v1'; + static String amplitudeSecret = 'c3d75b3e2520d708440acbb16b923e79'; + static String amplitudeServerZone = 'EU'; + static String posthogApiKey = 'phc_lVhbruaZpxiQxppHBJpL36ARnPlkqbCewv6cauoceTN'; + static String posthogHost = 'https://eu.i.posthog.com'; + static Locale defaultLocale = const Locale('en'); + static String defaultCurrency = 'EUR'; + static int defaultDimensionLength = 500; + static String clientId = ''; + static String wsProto = 'ws'; + static String wsEndpoint = '/ws'; + static Color themeColor = Color.fromARGB(255, 80, 63, 224); + static String nilObjectRef = '000000000000000000000000'; + + // Public getters for shared properties + static String get serviceUrl => '$apiProto://$apiHost'; + static String get apiUrl => '$serviceUrl$apiEndpoint'; + static String get wsUrl => '$wsProto://$apiHost$apiEndpoint$wsEndpoint'; + static const String accessTokenStorageKey = 'access_token'; + static const String refreshTokenStorageKey = 'refresh_token'; + static const String currentOrgKey = 'current_org'; + static const String deviceIdStorageKey = 'device_id'; + + // Method to apply the configuration, called by platform-specific implementations + static void applyConfiguration(Map configJson) { + apiProto = configJson['apiProto'] ?? apiProto; + apiHost = configJson['apiHost'] ?? apiHost; + apiEndpoint = configJson['apiEndpoint'] ?? apiEndpoint; + amplitudeSecret = configJson['amplitudeSecret'] ?? amplitudeSecret; + amplitudeServerZone = configJson['amplitudeServerZone'] ?? amplitudeServerZone; + posthogApiKey = configJson['posthogApiKey'] ?? posthogApiKey; + posthogHost = configJson['posthogHost'] ?? posthogHost; + defaultLocale = Locale(configJson['defaultLocale'] ?? defaultLocale.languageCode); + defaultCurrency = configJson['defaultCurrency'] ?? defaultCurrency; + wsProto = configJson['wsProto'] ?? wsProto; + wsEndpoint = configJson['wsEndpoint'] ?? wsEndpoint; + defaultDimensionLength = configJson['defaultDimensionLength'] ?? defaultDimensionLength; + clientId = configJson['clientId'] ?? clientId; + if (configJson.containsKey('themeColor')) { + themeColor = Color(int.parse(configJson['themeColor'])); + } + } +} diff --git a/frontend/pshared/lib/data/dto/ledger/account.dart b/frontend/pshared/lib/data/dto/ledger/account.dart index 566f6cd8..4064c777 100644 --- a/frontend/pshared/lib/data/dto/ledger/account.dart +++ b/frontend/pshared/lib/data/dto/ledger/account.dart @@ -2,6 +2,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:pshared/data/dto/describable.dart'; import 'package:pshared/data/dto/ledger/balance.dart'; +import 'package:pshared/data/dto/ledger/role.dart'; import 'package:pshared/data/dto/ledger/status.dart'; import 'package:pshared/data/dto/ledger/type.dart'; @@ -20,7 +21,8 @@ class LedgerAccountDTO { @JsonKey(fromJson: ledgerAccountStatusFromJson, toJson: ledgerAccountStatusToJson) final LedgerAccountStatusDTO status; final bool allowNegative; - final bool isSettlement; + @JsonKey(fromJson: ledgerAccountRoleFromJson, toJson: ledgerAccountRoleToJson) + final LedgerAccountRoleDTO role; final Map? metadata; final DateTime? createdAt; final DateTime? updatedAt; @@ -38,7 +40,7 @@ class LedgerAccountDTO { required this.currency, required this.status, required this.allowNegative, - required this.isSettlement, + required this.role, this.metadata, this.createdAt, this.updatedAt, diff --git a/frontend/pshared/lib/data/dto/ledger/role.dart b/frontend/pshared/lib/data/dto/ledger/role.dart new file mode 100644 index 00000000..16d3bb07 --- /dev/null +++ b/frontend/pshared/lib/data/dto/ledger/role.dart @@ -0,0 +1,107 @@ +import 'package:json_annotation/json_annotation.dart'; + + +enum LedgerAccountRoleDTO { + @JsonValue('unspecified') + unspecified, + + @JsonValue('operating') + operating, + + @JsonValue('hold') + hold, + + @JsonValue('transit') + transit, + + @JsonValue('settlement') + settlement, + + @JsonValue('clearing') + clearing, + + @JsonValue('pending') + pending, + + @JsonValue('reserve') + reserve, + + @JsonValue('liquidity') + liquidity, + + @JsonValue('fee') + fee, + + @JsonValue('chargeback') + chargeback, + + @JsonValue('adjustment') + adjustment, +} + +LedgerAccountRoleDTO ledgerAccountRoleFromJson(Object? value) { + final raw = value?.toString() ?? ''; + var normalized = raw.trim().toLowerCase(); + const prefix = 'account_role_'; + if (normalized.startsWith(prefix)) { + normalized = normalized.substring(prefix.length); + } + switch (normalized) { + case 'operating': + return LedgerAccountRoleDTO.operating; + case 'hold': + return LedgerAccountRoleDTO.hold; + case 'transit': + return LedgerAccountRoleDTO.transit; + case 'settlement': + return LedgerAccountRoleDTO.settlement; + case 'clearing': + return LedgerAccountRoleDTO.clearing; + case 'pending': + return LedgerAccountRoleDTO.pending; + case 'reserve': + return LedgerAccountRoleDTO.reserve; + case 'liquidity': + return LedgerAccountRoleDTO.liquidity; + case 'fee': + return LedgerAccountRoleDTO.fee; + case 'chargeback': + return LedgerAccountRoleDTO.chargeback; + case 'adjustment': + return LedgerAccountRoleDTO.adjustment; + case 'unspecified': + case '': + return LedgerAccountRoleDTO.unspecified; + default: + return LedgerAccountRoleDTO.unspecified; + } +} + +String ledgerAccountRoleToJson(LedgerAccountRoleDTO value) { + switch (value) { + case LedgerAccountRoleDTO.operating: + return 'operating'; + case LedgerAccountRoleDTO.hold: + return 'hold'; + case LedgerAccountRoleDTO.transit: + return 'transit'; + case LedgerAccountRoleDTO.settlement: + return 'settlement'; + case LedgerAccountRoleDTO.clearing: + return 'clearing'; + case LedgerAccountRoleDTO.pending: + return 'pending'; + case LedgerAccountRoleDTO.reserve: + return 'reserve'; + case LedgerAccountRoleDTO.liquidity: + return 'liquidity'; + case LedgerAccountRoleDTO.fee: + return 'fee'; + case LedgerAccountRoleDTO.chargeback: + return 'chargeback'; + case LedgerAccountRoleDTO.adjustment: + return 'adjustment'; + case LedgerAccountRoleDTO.unspecified: + return 'unspecified'; + } +} diff --git a/frontend/pshared/lib/data/mapper/ledger/account.dart b/frontend/pshared/lib/data/mapper/ledger/account.dart index eb6bc2c6..f49130ea 100644 --- a/frontend/pshared/lib/data/mapper/ledger/account.dart +++ b/frontend/pshared/lib/data/mapper/ledger/account.dart @@ -1,6 +1,7 @@ import 'package:pshared/data/dto/ledger/account.dart'; import 'package:pshared/data/mapper/describable.dart'; import 'package:pshared/data/mapper/ledger/balance.dart'; +import 'package:pshared/data/mapper/ledger/role.dart'; import 'package:pshared/data/mapper/ledger/status.dart'; import 'package:pshared/data/mapper/ledger/type.dart'; import 'package:pshared/models/describable.dart'; @@ -17,7 +18,7 @@ extension LedgerAccountDTOMapper on LedgerAccountDTO { currency: currency, status: status.toDomain(), allowNegative: allowNegative, - isSettlement: isSettlement, + role: role.toDomain(), metadata: metadata, createdAt: createdAt, updatedAt: updatedAt, @@ -36,7 +37,7 @@ extension LedgerAccountModelMapper on LedgerAccount { currency: currency, status: status.toDTO(), allowNegative: allowNegative, - isSettlement: isSettlement, + role: role.toDTO(), metadata: metadata, createdAt: createdAt, updatedAt: updatedAt, diff --git a/frontend/pshared/lib/data/mapper/ledger/role.dart b/frontend/pshared/lib/data/mapper/ledger/role.dart new file mode 100644 index 00000000..b7991093 --- /dev/null +++ b/frontend/pshared/lib/data/mapper/ledger/role.dart @@ -0,0 +1,65 @@ +import 'package:pshared/data/dto/ledger/role.dart'; +import 'package:pshared/models/ledger/role.dart'; + + +extension LedgerAccountRoleDTOMapper on LedgerAccountRoleDTO { + LedgerAccountRole toDomain() { + switch (this) { + case LedgerAccountRoleDTO.unspecified: + return LedgerAccountRole.unspecified; + case LedgerAccountRoleDTO.operating: + return LedgerAccountRole.operating; + case LedgerAccountRoleDTO.hold: + return LedgerAccountRole.hold; + case LedgerAccountRoleDTO.transit: + return LedgerAccountRole.transit; + case LedgerAccountRoleDTO.settlement: + return LedgerAccountRole.settlement; + case LedgerAccountRoleDTO.clearing: + return LedgerAccountRole.clearing; + case LedgerAccountRoleDTO.pending: + return LedgerAccountRole.pending; + case LedgerAccountRoleDTO.reserve: + return LedgerAccountRole.reserve; + case LedgerAccountRoleDTO.liquidity: + return LedgerAccountRole.liquidity; + case LedgerAccountRoleDTO.fee: + return LedgerAccountRole.fee; + case LedgerAccountRoleDTO.chargeback: + return LedgerAccountRole.chargeback; + case LedgerAccountRoleDTO.adjustment: + return LedgerAccountRole.adjustment; + } + } +} + +extension LedgerAccountRoleModelMapper on LedgerAccountRole { + LedgerAccountRoleDTO toDTO() { + switch (this) { + case LedgerAccountRole.unspecified: + return LedgerAccountRoleDTO.unspecified; + case LedgerAccountRole.operating: + return LedgerAccountRoleDTO.operating; + case LedgerAccountRole.hold: + return LedgerAccountRoleDTO.hold; + case LedgerAccountRole.transit: + return LedgerAccountRoleDTO.transit; + case LedgerAccountRole.settlement: + return LedgerAccountRoleDTO.settlement; + case LedgerAccountRole.clearing: + return LedgerAccountRoleDTO.clearing; + case LedgerAccountRole.pending: + return LedgerAccountRoleDTO.pending; + case LedgerAccountRole.reserve: + return LedgerAccountRoleDTO.reserve; + case LedgerAccountRole.liquidity: + return LedgerAccountRoleDTO.liquidity; + case LedgerAccountRole.fee: + return LedgerAccountRoleDTO.fee; + case LedgerAccountRole.chargeback: + return LedgerAccountRoleDTO.chargeback; + case LedgerAccountRole.adjustment: + return LedgerAccountRoleDTO.adjustment; + } + } +} diff --git a/frontend/pshared/lib/data/mapper/payment/enums.dart b/frontend/pshared/lib/data/mapper/payment/enums.dart index 1b1a4f20..cd3f236d 100644 --- a/frontend/pshared/lib/data/mapper/payment/enums.dart +++ b/frontend/pshared/lib/data/mapper/payment/enums.dart @@ -90,6 +90,8 @@ ChainNetwork chainNetworkFromValue(String? value) { return ChainNetwork.ethereumMainnet; case 'arbitrum_one': return ChainNetwork.arbitrumOne; + case 'arbitrum_sepolia': + return ChainNetwork.arbitrumSepolia; case 'tron_mainnet': return ChainNetwork.tronMainnet; case 'tron_nile': @@ -111,6 +113,8 @@ String chainNetworkToValue(ChainNetwork chain) { return 'tron_mainnet'; case ChainNetwork.tronNile: return 'tron_nile'; + case ChainNetwork.arbitrumSepolia: + return 'arbitrum_sepolia'; case ChainNetwork.unspecified: return 'unspecified'; } diff --git a/frontend/pshared/lib/l10n/en.arb b/frontend/pshared/lib/l10n/en.arb index c963da74..21b64145 100644 --- a/frontend/pshared/lib/l10n/en.arb +++ b/frontend/pshared/lib/l10n/en.arb @@ -46,6 +46,11 @@ "description": "Label for the Arbitrum One network" }, + "chainNetworkArbitrumSepolia": "Arbitrum Sepolia", + "@chainNetworkArbitrumSepolia": { + "description": "Label for the Arbitrum Sepolia network" + }, + "chainNetworkTronMainnet": "Tron Mainnet", "@chainNetworkTronMainnet": { "description": "Label for the Tron mainnet network" diff --git a/frontend/pshared/lib/l10n/ru.arb b/frontend/pshared/lib/l10n/ru.arb index 83e9f943..ce6efa49 100644 --- a/frontend/pshared/lib/l10n/ru.arb +++ b/frontend/pshared/lib/l10n/ru.arb @@ -46,6 +46,11 @@ "description": "Label for the Arbitrum One network" }, + "chainNetworkArbitrumSepolia": "Arbitrum Sepolia", + "@chainNetworkArbitrumSepolia": { + "description": "Label for the Arbitrum Sepolia network" + }, + "chainNetworkTronMainnet": "Tron Mainnet", "@chainNetworkTronMainnet": { "description": "Label for the Tron mainnet network" diff --git a/frontend/pshared/lib/models/ledger/account.dart b/frontend/pshared/lib/models/ledger/account.dart index b1ec7ae9..a01ed7da 100644 --- a/frontend/pshared/lib/models/ledger/account.dart +++ b/frontend/pshared/lib/models/ledger/account.dart @@ -1,5 +1,6 @@ import 'package:pshared/models/describable.dart'; import 'package:pshared/models/ledger/balance.dart'; +import 'package:pshared/models/ledger/role.dart'; import 'package:pshared/models/ledger/status.dart'; import 'package:pshared/models/ledger/type.dart'; @@ -13,7 +14,7 @@ class LedgerAccount implements Describable { final String currency; final LedgerAccountStatus status; final bool allowNegative; - final bool isSettlement; + final LedgerAccountRole role; final Map? metadata; final DateTime? createdAt; final DateTime? updatedAt; @@ -35,7 +36,7 @@ class LedgerAccount implements Describable { required this.currency, required this.status, required this.allowNegative, - required this.isSettlement, + required this.role, this.metadata, this.createdAt, this.updatedAt, @@ -55,7 +56,7 @@ class LedgerAccount implements Describable { currency: currency, status: status, allowNegative: allowNegative, - isSettlement: isSettlement, + role: role, metadata: metadata, createdAt: createdAt, updatedAt: updatedAt, diff --git a/frontend/pshared/lib/models/ledger/role.dart b/frontend/pshared/lib/models/ledger/role.dart new file mode 100644 index 00000000..2591adef --- /dev/null +++ b/frontend/pshared/lib/models/ledger/role.dart @@ -0,0 +1,14 @@ +enum LedgerAccountRole { + unspecified, + operating, + hold, + transit, + settlement, + clearing, + pending, + reserve, + liquidity, + fee, + chargeback, + adjustment, +} diff --git a/frontend/pshared/lib/models/pagination/cursor_page.dart b/frontend/pshared/lib/models/pagination/cursor_page.dart new file mode 100644 index 00000000..5e29d7ad --- /dev/null +++ b/frontend/pshared/lib/models/pagination/cursor_page.dart @@ -0,0 +1,6 @@ +class CursorPage { + final List items; + final String? nextCursor; + + const CursorPage({required this.items, required this.nextCursor}); +} diff --git a/frontend/pshared/lib/models/payment/chain_network.dart b/frontend/pshared/lib/models/payment/chain_network.dart index b1b0e883..5184c7d2 100644 --- a/frontend/pshared/lib/models/payment/chain_network.dart +++ b/frontend/pshared/lib/models/payment/chain_network.dart @@ -2,6 +2,7 @@ enum ChainNetwork { unspecified, ethereumMainnet, arbitrumOne, + arbitrumSepolia, tronMainnet, - tronNile + tronNile, } diff --git a/frontend/pshared/lib/models/payment/page.dart b/frontend/pshared/lib/models/payment/page.dart new file mode 100644 index 00000000..7a13e666 --- /dev/null +++ b/frontend/pshared/lib/models/payment/page.dart @@ -0,0 +1,5 @@ +import 'package:pshared/models/pagination/cursor_page.dart'; +import 'package:pshared/models/payment/payment.dart'; + + +typedef PaymentPage =CursorPage; \ No newline at end of file diff --git a/frontend/pshared/lib/provider/ledger.dart b/frontend/pshared/lib/provider/ledger.dart index a955f029..66c6fb7d 100644 --- a/frontend/pshared/lib/provider/ledger.dart +++ b/frontend/pshared/lib/provider/ledger.dart @@ -8,6 +8,7 @@ import 'package:collection/collection.dart'; import 'package:pshared/models/currency.dart'; import 'package:pshared/models/describable.dart'; import 'package:pshared/models/ledger/account.dart'; +import 'package:pshared/models/ledger/role.dart'; import 'package:pshared/models/payment/wallet.dart'; import 'package:pshared/provider/organizations.dart'; import 'package:pshared/provider/resource.dart'; @@ -24,7 +25,7 @@ class LedgerAccountsProvider with ChangeNotifier { Resource> _resource = Resource(data: []); Resource> get resource => _resource; - List get accounts => (_resource.data ?? []).whereNot((la)=> la.isSettlement).toList(); + List get accounts => (_resource.data ?? []).where((la) => la.role == LedgerAccountRole.operating).toList(); bool get isLoading => _resource.isLoading; Exception? get error => _resource.error; diff --git a/frontend/pshared/lib/provider/payment/payments.dart b/frontend/pshared/lib/provider/payment/payments.dart new file mode 100644 index 00000000..bfde3dbe --- /dev/null +++ b/frontend/pshared/lib/provider/payment/payments.dart @@ -0,0 +1,181 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +import 'package:pshared/models/payment/payment.dart'; +import 'package:pshared/provider/organizations.dart'; +import 'package:pshared/provider/resource.dart'; +import 'package:pshared/service/payment/service.dart'; +import 'package:pshared/utils/exception.dart'; + + +class PaymentsProvider with ChangeNotifier { + OrganizationsProvider? _organizations; + String? _loadedOrganizationRef; + + Resource> _resource = Resource(data: []); + bool _isLoaded = false; + bool _isLoadingMore = false; + String? _nextCursor; + + int? _limit; + String? _sourceRef; + String? _destinationRef; + List? _states; + + int _opSeq = 0; + + Resource> get resource => _resource; + List get payments => _resource.data ?? []; + bool get isLoading => _resource.isLoading; + Exception? get error => _resource.error; + bool get isReady => _isLoaded && !_resource.isLoading && _resource.error == null; + + bool get isLoadingMore => _isLoadingMore; + String? get nextCursor => _nextCursor; + bool get canLoadMore => _nextCursor != null && _nextCursor!.isNotEmpty; + + void update(OrganizationsProvider organizations) { + _organizations = organizations; + if (!organizations.isOrganizationSet) { + reset(); + return; + } + + final orgRef = organizations.current.id; + if (_loadedOrganizationRef != orgRef) { + _loadedOrganizationRef = orgRef; + unawaited(refresh()); + } + } + + Future refresh({ + int? limit, + String? sourceRef, + String? destinationRef, + List? states, + }) async { + final org = _organizations; + if (org == null || !org.isOrganizationSet) return; + + _limit = limit; + _sourceRef = _normalize(sourceRef); + _destinationRef = _normalize(destinationRef); + _states = _normalizeStates(states); + _nextCursor = null; + _isLoadingMore = false; + + final seq = ++_opSeq; + + _applyResource(_resource.copyWith(isLoading: true, error: null), notify: true); + + try { + final page = await PaymentService.listPage( + org.current.id, + limit: _limit, + cursor: null, + sourceRef: _sourceRef, + destinationRef: _destinationRef, + states: _states, + ); + + if (seq != _opSeq) return; + + _isLoaded = true; + _nextCursor = _normalize(page.nextCursor); + _applyResource( + Resource(data: page.items, isLoading: false, error: null), + notify: true, + ); + } catch (e) { + if (seq != _opSeq) return; + + _applyResource( + _resource.copyWith(isLoading: false, error: toException(e)), + notify: true, + ); + } + } + + Future loadMore() async { + final org = _organizations; + if (org == null || !org.isOrganizationSet) return; + if (_isLoadingMore || _resource.isLoading) return; + + final cursor = _normalize(_nextCursor); + if (cursor == null) return; + + final seq = _opSeq; + + _isLoadingMore = true; + _applyResource(_resource.copyWith(error: null), notify: false); + notifyListeners(); + + try { + final page = await PaymentService.listPage( + org.current.id, + limit: _limit, + cursor: cursor, + sourceRef: _sourceRef, + destinationRef: _destinationRef, + states: _states, + ); + + if (seq != _opSeq) return; + + final combined = List.from(payments)..addAll(page.items); + _nextCursor = _normalize(page.nextCursor); + + _applyResource( + _resource.copyWith(data: combined, error: null), + notify: false, + ); + } catch (e) { + if (seq != _opSeq) return; + + _applyResource( + _resource.copyWith(error: toException(e)), + notify: false, + ); + } finally { + if (seq == _opSeq) { + _isLoadingMore = false; + notifyListeners(); + } + } + } + + void reset() { + _opSeq++; + _isLoaded = false; + _isLoadingMore = false; + _nextCursor = null; + _limit = null; + _sourceRef = null; + _destinationRef = null; + _states = null; + _resource = Resource(data: []); + notifyListeners(); + } + + void _applyResource(Resource> newResource, {required bool notify}) { + _resource = newResource; + if (notify) notifyListeners(); + } + + String? _normalize(String? value) { + final trimmed = value?.trim(); + if (trimmed == null || trimmed.isEmpty) return null; + return trimmed; + } + + List? _normalizeStates(List? states) { + if (states == null || states.isEmpty) return null; + final normalized = states + .map((state) => state.trim()) + .where((state) => state.isNotEmpty) + .toList(); + if (normalized.isEmpty) return null; + return normalized; + } +} diff --git a/frontend/pshared/lib/provider/payment/provider.dart b/frontend/pshared/lib/provider/payment/provider.dart index 10615049..8b29e903 100644 --- a/frontend/pshared/lib/provider/payment/provider.dart +++ b/frontend/pshared/lib/provider/payment/provider.dart @@ -5,6 +5,7 @@ import 'package:pshared/provider/organizations.dart'; import 'package:pshared/provider/payment/quotation/quotation.dart'; import 'package:pshared/provider/resource.dart'; import 'package:pshared/service/payment/service.dart'; +import 'package:pshared/utils/exception.dart'; class PaymentProvider extends ChangeNotifier { @@ -53,11 +54,7 @@ class PaymentProvider extends ChangeNotifier { _isLoaded = true; _setResource(_payment.copyWith(data: response, isLoading: false, error: null)); } catch (e) { - _setResource(_payment.copyWith( - data: null, - error: e is Exception ? e : Exception(e.toString()), - isLoading: false, - )); + _setResource(_payment.copyWith(data: null, error: toException(e), isLoading: false)); } return _payment.data; } diff --git a/frontend/pshared/lib/provider/payment/quotation/intent_builder.dart b/frontend/pshared/lib/provider/payment/quotation/intent_builder.dart index 631b6760..b205bb03 100644 --- a/frontend/pshared/lib/provider/payment/quotation/intent_builder.dart +++ b/frontend/pshared/lib/provider/payment/quotation/intent_builder.dart @@ -1,4 +1,6 @@ import 'package:pshared/controllers/balance_mask/wallets.dart'; +import 'package:pshared/models/payment/asset.dart'; +import 'package:pshared/models/payment/chain_network.dart'; import 'package:pshared/models/payment/currency_pair.dart'; import 'package:pshared/models/payment/customer.dart'; import 'package:pshared/models/payment/fx/intent.dart'; @@ -55,6 +57,10 @@ class QuotationIntentBuilder { destination: paymentData, source: ManagedWalletPaymentMethod( managedWalletRef: selectedWallet.id, + asset: PaymentAsset( + tokenSymbol: selectedWallet.tokenSymbol ?? '', + chain: selectedWallet.network ?? ChainNetwork.unspecified, + ) ), fx: fxIntent, settlementMode: payment.payerCoversFee ? SettlementMode.fixReceived : SettlementMode.fixSource, diff --git a/frontend/pshared/lib/provider/permissions.dart b/frontend/pshared/lib/provider/permissions.dart index d8e90298..8700545f 100644 --- a/frontend/pshared/lib/provider/permissions.dart +++ b/frontend/pshared/lib/provider/permissions.dart @@ -18,6 +18,7 @@ import 'package:pshared/models/resources.dart'; import 'package:pshared/provider/organizations.dart'; import 'package:pshared/provider/resource.dart'; import 'package:pshared/service/permissions.dart'; +import 'package:pshared/utils/exception.dart'; class PermissionsProvider extends ChangeNotifier { @@ -43,10 +44,7 @@ class PermissionsProvider extends ChangeNotifier { await operation(); return await load(); } catch (e) { - _userAccess = _userAccess.copyWith( - error: e is Exception ? e : Exception(e.toString()), - isLoading: false, - ); + _userAccess = _userAccess.copyWith(error: toException(e), isLoading: false); notifyListeners(); return _userAccess.data; } diff --git a/frontend/pshared/lib/service/ledger.dart b/frontend/pshared/lib/service/ledger.dart index d8374296..36541419 100644 --- a/frontend/pshared/lib/service/ledger.dart +++ b/frontend/pshared/lib/service/ledger.dart @@ -4,11 +4,13 @@ import 'package:pshared/api/responses/ledger/balance.dart'; import 'package:pshared/data/mapper/describable.dart'; import 'package:pshared/data/mapper/ledger/account.dart'; import 'package:pshared/data/mapper/ledger/balance.dart'; +import 'package:pshared/data/mapper/ledger/role.dart'; import 'package:pshared/data/mapper/ledger/type.dart'; import 'package:pshared/models/currency.dart'; import 'package:pshared/models/describable.dart'; import 'package:pshared/models/ledger/account.dart'; import 'package:pshared/models/ledger/balance.dart'; +import 'package:pshared/models/ledger/role.dart'; import 'package:pshared/models/ledger/type.dart'; import 'package:pshared/service/authorization/service.dart'; import 'package:pshared/service/services.dart'; @@ -49,7 +51,7 @@ class LedgerService { describable: describable.toDTO(), ownerRef: ownerRef, allowNegative: false, - isSettlement: false, + role: LedgerAccountRole.operating.toDTO(), accountType: LedgerAccountType.asset.toDTO(), currency: currencyCodeToString(currency), ).toJson(), diff --git a/frontend/pshared/lib/service/payment/service.dart b/frontend/pshared/lib/service/payment/service.dart index f47787cb..0c5856ea 100644 --- a/frontend/pshared/lib/service/payment/service.dart +++ b/frontend/pshared/lib/service/payment/service.dart @@ -6,16 +6,18 @@ import 'package:pshared/api/requests/payment/initiate.dart'; import 'package:pshared/api/responses/payment/payment.dart'; import 'package:pshared/api/responses/payment/payments.dart'; import 'package:pshared/data/mapper/payment/payment_response.dart'; +import 'package:pshared/models/payment/page.dart'; import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/service/authorization/service.dart'; import 'package:pshared/service/services.dart'; +import 'package:pshared/utils/http/params.dart'; class PaymentService { static final _logger = Logger('service.payment'); static const String _objectType = Services.payments; - static Future> list( + static Future listPage( String organizationRef, { int? limit, String? cursor, @@ -25,12 +27,6 @@ class PaymentService { }) async { _logger.fine('Listing payments for organization $organizationRef'); final queryParams = {}; - if (limit != null) { - queryParams['limit'] = limit.toString(); - } - if (cursor != null && cursor.isNotEmpty) { - queryParams['cursor'] = cursor; - } if (sourceRef != null && sourceRef.isNotEmpty) { queryParams['source_ref'] = sourceRef; } @@ -41,12 +37,35 @@ class PaymentService { queryParams['state'] = states.join(','); } - final path = '/$organizationRef'; - final url = queryParams.isEmpty - ? path - : Uri(path: path, queryParameters: queryParams).toString(); + final url = cursorParamsToUriString( + path: '/$organizationRef', + limit: limit, + cursor: cursor, + queryParams: queryParams, + ); final response = await AuthorizationService.getGETResponse(_objectType, url); - return PaymentsResponse.fromJson(response).payments.map((payment) => payment.toDomain()).toList(); + final parsed = PaymentsResponse.fromJson(response); + final payments = parsed.payments.map((payment) => payment.toDomain()).toList(); + return PaymentPage(items: payments, nextCursor: parsed.nextCursor); + } + + static Future> list( + String organizationRef, { + int? limit, + String? cursor, + String? sourceRef, + String? destinationRef, + List? states, + }) async { + final page = await listPage( + organizationRef, + limit: limit, + cursor: cursor, + sourceRef: sourceRef, + destinationRef: destinationRef, + states: states, + ); + return page.items; } static Future pay( @@ -68,4 +87,5 @@ class PaymentService { ); return PaymentResponse.fromJson(response).payment.toDomain(); } + } diff --git a/frontend/pshared/lib/utils/http/params.dart b/frontend/pshared/lib/utils/http/params.dart index 195eb771..66c6ea0f 100644 --- a/frontend/pshared/lib/utils/http/params.dart +++ b/frontend/pshared/lib/utils/http/params.dart @@ -2,6 +2,7 @@ const String _limitParam = 'limit'; const String _offsetParam = 'offset'; const String _archivedParam = 'archived'; +const String _cursorParam = 'cursor'; void _addIfNotNull(Map params, String key, dynamic value) { if (value != null) { @@ -9,6 +10,13 @@ void _addIfNotNull(Map params, String key, dynamic value) { } } +void _addIfNotBlank(Map params, String key, String? value) { + final trimmed = value?.trim(); + if (trimmed != null && trimmed.isNotEmpty) { + params[key] = trimmed; + } +} + Uri paramsToUri({ required String path, int? limit, @@ -36,3 +44,32 @@ String paramsToUriString({ int? offset, bool? fetchArchived, }) => paramsToUri(path: path, limit: limit, offset: offset, fetchArchived: fetchArchived).toString(); + +Uri cursorParamsToUri({ + required String path, + int? limit, + String? cursor, + Map queryParams = const {}, +}) { + final params = Map.from(queryParams); + _addIfNotNull(params, _limitParam, limit); + _addIfNotBlank(params, _cursorParam, cursor); + params.removeWhere((_, value) => value.trim().isEmpty); + + return Uri( + path: path, + queryParameters: params.isEmpty ? null : params, + ); +} + +String cursorParamsToUriString({ + required String path, + int? limit, + String? cursor, + Map queryParams = const {}, +}) => cursorParamsToUri( + path: path, + limit: limit, + cursor: cursor, + queryParams: queryParams, +).toString(); diff --git a/frontend/pshared/lib/utils/l10n/chain.dart b/frontend/pshared/lib/utils/l10n/chain.dart index 08244b90..675f1f64 100644 --- a/frontend/pshared/lib/utils/l10n/chain.dart +++ b/frontend/pshared/lib/utils/l10n/chain.dart @@ -15,6 +15,8 @@ extension ChainNetworkL10n on ChainNetwork { return l10n.chainNetworkEthereumMainnet; case ChainNetwork.arbitrumOne: return l10n.chainNetworkArbitrumOne; + case ChainNetwork.arbitrumSepolia: + return l10n.chainNetworkArbitrumSepolia; case ChainNetwork.tronMainnet: return l10n.chainNetworkTronMainnet; case ChainNetwork.tronNile: diff --git a/frontend/pweb/lib/main.dart b/frontend/pweb/lib/main.dart index 2c6816c1..9e2570b6 100644 --- a/frontend/pweb/lib/main.dart +++ b/frontend/pweb/lib/main.dart @@ -20,6 +20,7 @@ import 'package:pshared/provider/recipient/provider.dart'; import 'package:pshared/provider/email_verification.dart'; import 'package:pshared/provider/ledger.dart'; import 'package:pshared/provider/payment/wallets.dart'; +import 'package:pshared/provider/payment/payments.dart'; import 'package:pshared/provider/invitations.dart'; import 'package:pshared/service/ledger.dart'; import 'package:pshared/service/payment/wallets.dart'; @@ -27,11 +28,9 @@ import 'package:pshared/service/payment/wallets.dart'; import 'package:pweb/app/app.dart'; import 'package:pweb/pages/invitations/widgets/list/view_model.dart'; import 'package:pweb/app/timeago.dart'; -import 'package:pweb/providers/operatioins.dart'; import 'package:pweb/providers/two_factor.dart'; import 'package:pweb/providers/upload_history.dart'; import 'package:pweb/providers/wallet_transactions.dart'; -import 'package:pweb/services/operations.dart'; import 'package:pweb/services/payments/history.dart'; import 'package:pweb/services/posthog.dart'; import 'package:pweb/services/wallet_transactions.dart'; @@ -78,6 +77,10 @@ void main() async { create: (_) => EmployeesProvider(), update: (context, organizations, provider) => provider!..updateProviders(organizations), ), + ChangeNotifierProxyProvider( + create: (_) => PaymentsProvider(), + update: (context, organizations, provider) => provider!..update(organizations), + ), ChangeNotifierProvider(create: (_) => EmailVerificationProvider()), ChangeNotifierProvider( @@ -117,9 +120,6 @@ void main() async { ChangeNotifierProvider( create: (_) => WalletTransactionsProvider(MockWalletTransactionsService())..load(), ), - ChangeNotifierProvider( - create: (_) => OperationProvider(OperationService())..loadOperations(), - ), ], child: const PayApp(), ), diff --git a/frontend/pweb/lib/pages/report/page.dart b/frontend/pweb/lib/pages/report/page.dart index 3b14e662..a9223db6 100644 --- a/frontend/pweb/lib/pages/report/page.dart +++ b/frontend/pweb/lib/pages/report/page.dart @@ -2,11 +2,15 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:pshared/models/payment/operation.dart'; +import 'package:pshared/models/payment/payment.dart'; +import 'package:pshared/models/payment/status.dart'; +import 'package:pshared/provider/payment/payments.dart'; + import 'package:pweb/pages/report/charts/distribution.dart'; import 'package:pweb/pages/report/charts/status.dart'; import 'package:pweb/pages/report/table/filters.dart'; import 'package:pweb/pages/report/table/widget.dart'; -import 'package:pweb/providers/operatioins.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; @@ -19,18 +23,25 @@ class OperationHistoryPage extends StatefulWidget { } class _OperationHistoryPageState extends State { + DateTimeRange? _pendingRange; + DateTimeRange? _appliedRange; + final Set _pendingStatuses = {}; + Set _appliedStatuses = {}; + @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - context.read().loadOperations(); + final provider = context.read(); + if (!provider.isReady && !provider.isLoading) { + provider.refresh(); + } }); } Future _pickRange() async { - final provider = context.read(); final now = DateTime.now(); - final initial = provider.dateRange ?? + final initial = _pendingRange ?? DateTimeRange( start: now.subtract(const Duration(days: 30)), end: now, @@ -44,33 +55,157 @@ class _OperationHistoryPageState extends State { ); if (picked != null) { - provider.setDateRange(picked); + setState(() { + _pendingRange = picked; + }); } } + void _toggleStatus(OperationStatus status) { + setState(() { + if (_pendingStatuses.contains(status)) { + _pendingStatuses.remove(status); + } else { + _pendingStatuses.add(status); + } + }); + } + + void _applyFilters() { + setState(() { + _appliedRange = _pendingRange; + _appliedStatuses = {..._pendingStatuses}; + }); + } + + List _mapPayments(List payments) { + return payments.map(_mapPayment).toList(); + } + + OperationItem _mapPayment(Payment payment) { + final debit = payment.lastQuote?.debitAmount; + final settlement = payment.lastQuote?.expectedSettlementAmount; + final amountMoney = debit ?? settlement; + + final amount = _parseAmount(amountMoney?.amount); + final currency = amountMoney?.currency ?? ''; + final toAmount = settlement == null ? amount : _parseAmount(settlement.amount); + final toCurrency = settlement?.currency ?? currency; + + final payId = _firstNonEmpty([payment.paymentRef, payment.idempotencyKey]) ?? '-'; + final name = _firstNonEmpty([payment.lastQuote?.quoteRef, payment.paymentRef, payment.idempotencyKey]) ?? '-'; + final comment = _firstNonEmpty([payment.failureReason, payment.failureCode, payment.state]) ?? ''; + + return OperationItem( + status: _statusFromPaymentState(payment.state), + fileName: null, + amount: amount, + currency: currency, + toAmount: toAmount, + toCurrency: toCurrency, + payId: payId, + cardNumber: null, + name: name, + date: _resolvePaymentDate(payment), + comment: comment, + ); + } + + List _filterOperations(List operations) { + if (_appliedRange == null && _appliedStatuses.isEmpty) { + return operations; + } + + return operations.where((op) { + final statusMatch = + _appliedStatuses.isEmpty || _appliedStatuses.contains(op.status); + + final dateMatch = _appliedRange == null || + _isUnknownDate(op.date) || + (op.date.isAfter(_appliedRange!.start.subtract(const Duration(seconds: 1))) && + op.date.isBefore(_appliedRange!.end.add(const Duration(seconds: 1)))); + + return statusMatch && dateMatch; + }).toList(); + } + + OperationStatus _statusFromPaymentState(String? raw) { + final state = raw?.trim().toLowerCase(); + + switch (state) { + case 'accepted': + case 'funds_reserved': + case 'submitted': + case 'unspecified': + case null: + return OperationStatus.processing; + + case 'settled': + return OperationStatus.success; + + case 'failed': + case 'cancelled': + return OperationStatus.error; + + default: + // Future-proof: any new backend state is treated as processing + return OperationStatus.processing; + } + } + + DateTime _resolvePaymentDate(Payment payment) { + final expiresAt = payment.lastQuote?.fxQuote?.expiresAtUnixMs; + if (expiresAt != null && expiresAt > 0) { + return DateTime.fromMillisecondsSinceEpoch(expiresAt, isUtc: true); + } + return DateTime.fromMillisecondsSinceEpoch(0, isUtc: true); + } + + double _parseAmount(String? amount) { + if (amount == null || amount.trim().isEmpty) return 0; + return double.tryParse(amount) ?? 0; + } + + String? _firstNonEmpty(List values) { + for (final value in values) { + final trimmed = value?.trim(); + if (trimmed != null && trimmed.isNotEmpty) return trimmed; + } + return null; + } + + bool _isUnknownDate(DateTime date) => date.millisecondsSinceEpoch == 0; + @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; - return Consumer( + return Consumer( builder: (context, provider, child) { if (provider.isLoading) { return const Center(child: CircularProgressIndicator()); } if (provider.error != null) { + final message = provider.error?.toString() ?? loc.noErrorInformation; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text(loc.notificationError(provider.error ?? loc.noErrorInformation)), + Text(loc.notificationError(message)), ElevatedButton( - onPressed: () => provider.loadOperations(), + onPressed: () => provider.refresh(), child: Text(loc.retry), ), ], ), ); } + + final operations = _mapPayments(provider.payments); + final filteredOperations = _filterOperations(operations); + final hasFileName = operations.any( + (operation) => (operation.fileName ?? '').trim().isNotEmpty, + ); return Padding( padding: const EdgeInsets.all(16.0), @@ -84,26 +219,26 @@ class _OperationHistoryPageState extends State { spacing: 16, children: [ Expanded( - child: StatusChart(operations: provider.allOperations), + child: StatusChart(operations: operations), ), Expanded( child: PayoutDistributionChart( - operations: provider.allOperations, + operations: operations, ), ), ], ), ), OperationFilters( - selectedRange: provider.dateRange, - selectedStatuses: provider.selectedStatuses, + selectedRange: _pendingRange, + selectedStatuses: _pendingStatuses, onPickRange: _pickRange, - onToggleStatus: provider.toggleStatus, - onApply: () => provider.applyFilters(context), + onToggleStatus: _toggleStatus, + onApply: _applyFilters, ), OperationsTable( - operations: provider.filteredOperations, - showFileNameColumn: provider.hasFileName, + operations: filteredOperations, + showFileNameColumn: hasFileName, ), ], ), diff --git a/frontend/pweb/lib/pages/report/table/filters.dart b/frontend/pweb/lib/pages/report/table/filters.dart index 4c4583d2..1545b85f 100644 --- a/frontend/pweb/lib/pages/report/table/filters.dart +++ b/frontend/pweb/lib/pages/report/table/filters.dart @@ -6,10 +6,10 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class OperationFilters extends StatelessWidget { final DateTimeRange? selectedRange; - final Set selectedStatuses; + final Set selectedStatuses; final VoidCallback onPickRange; final VoidCallback onApply; - final ValueChanged onToggleStatus; + final ValueChanged onToggleStatus; const OperationFilters({ super.key, @@ -66,11 +66,12 @@ class OperationFilters extends StatelessWidget { Wrap( spacing: 12, runSpacing: 8, - children: [ - OperationStatus.success.localized(context), - OperationStatus.processing.localized(context), - OperationStatus.error.localized(context), + children: const [ + OperationStatus.success, + OperationStatus.processing, + OperationStatus.error, ].map((status) { + final label = status.localized(context); final isSelected = selectedStatuses.contains(status); return GestureDetector( onTap: () => onToggleStatus(status), @@ -89,7 +90,7 @@ class OperationFilters extends StatelessWidget { vertical: 4, ), child: Text( - l10n.status(status), + l10n.status(label), style: TextStyle( color: isSelected ? Colors.white : Colors.black87, fontSize: 14, diff --git a/frontend/pweb/lib/pages/report/table/row.dart b/frontend/pweb/lib/pages/report/table/row.dart index 900c0437..e7715e25 100644 --- a/frontend/pweb/lib/pages/report/table/row.dart +++ b/frontend/pweb/lib/pages/report/table/row.dart @@ -8,6 +8,13 @@ import 'package:pweb/pages/report/table/badge.dart'; class OperationRow { static DataRow build(OperationItem op, BuildContext context) { + final isUnknownDate = op.date.millisecondsSinceEpoch == 0; + final localDate = op.date.toLocal(); + final dateLabel = isUnknownDate + ? '-' + : '${TimeOfDay.fromDateTime(localDate).format(context)}\n' + '${localDate.toIso8601String().split("T").first}'; + return DataRow(cells: [ DataCell(OperationStatusBadge(status: op.status)), DataCell(Text(op.fileName ?? '')), @@ -16,10 +23,7 @@ class OperationRow { DataCell(Text(op.payId)), DataCell(Text(op.cardNumber ?? '-')), DataCell(Text(op.name)), - DataCell(Text( - '${TimeOfDay.fromDateTime(op.date).format(context)}\n' - '${op.date.toLocal().toIso8601String().split("T").first}', - )), + DataCell(Text(dateLabel)), DataCell(Text(op.comment)), ]); }