front dev update

This commit is contained in:
Stephan D
2026-01-30 16:54:56 +01:00
parent 51f5b0804a
commit 102c5d3668
31 changed files with 755 additions and 74 deletions

15
.gitignore vendored
View File

@@ -8,6 +8,19 @@ devtools_options.yaml
untranslated.txt untranslated.txt
generate_protos.sh generate_protos.sh
update_dep.sh update_dep.sh
test.sh
.vscode/ .vscode/
.gocache/ .gocache/
.cache/ .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

View File

@@ -1,6 +1,7 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/data/dto/describable.dart'; import 'package:pshared/data/dto/describable.dart';
import 'package:pshared/data/dto/ledger/role.dart';
import 'package:pshared/data/dto/ledger/type.dart'; import 'package:pshared/data/dto/ledger/type.dart';
part 'create.g.dart'; part 'create.g.dart';
@@ -11,7 +12,7 @@ class CreateLedgerAccountRequest {
final Map<String, String>? metadata; final Map<String, String>? metadata;
final String currency; final String currency;
final bool allowNegative; final bool allowNegative;
final bool isSettlement; final LedgerAccountRoleDTO role;
final DescribableDTO describable; final DescribableDTO describable;
final String? ownerRef; final String? ownerRef;
final LedgerAccountTypeDTO accountType; final LedgerAccountTypeDTO accountType;
@@ -20,7 +21,7 @@ class CreateLedgerAccountRequest {
this.metadata, this.metadata,
required this.currency, required this.currency,
required this.allowNegative, required this.allowNegative,
required this.isSettlement, required this.role,
required this.describable, required this.describable,
required this.accountType, required this.accountType,
this.ownerRef, this.ownerRef,

View File

@@ -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<String, dynamic> json) => _$CursorPageResponseFromJson(json);
@override
Map<String, dynamic> toJson() => _$CursorPageResponseToJson(this);
}

View File

@@ -1,6 +1,6 @@
import 'package:json_annotation/json_annotation.dart'; 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/api/responses/token.dart';
import 'package:pshared/data/dto/payment/payment.dart'; import 'package:pshared/data/dto/payment/payment.dart';
@@ -8,11 +8,14 @@ part 'payments.g.dart';
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
class PaymentsResponse extends BaseAuthorizedResponse { class PaymentsResponse extends CursorPageResponse {
final List<PaymentDTO> payments; final List<PaymentDTO> payments;
const PaymentsResponse({required super.accessToken, required this.payments}); const PaymentsResponse({
required super.accessToken,
required super.nextCursor,
required this.payments,
});
factory PaymentsResponse.fromJson(Map<String, dynamic> json) => _$PaymentsResponseFromJson(json); factory PaymentsResponse.fromJson(Map<String, dynamic> json) => _$PaymentsResponseFromJson(json);
@override @override

View File

@@ -4,10 +4,6 @@ import 'package:flutter/material.dart';
class CommonConstants { class CommonConstants {
static String apiProto = 'https'; static String apiProto = 'https';
static String apiHost = 'app.sendico.io'; 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 apiEndpoint = '/api/v1';
static String amplitudeSecret = 'c3d75b3e2520d708440acbb16b923e79'; static String amplitudeSecret = 'c3d75b3e2520d708440acbb16b923e79';
static String amplitudeServerZone = 'EU'; static String amplitudeServerZone = 'EU';

View File

@@ -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<String, dynamic> 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']));
}
}
}

View File

@@ -2,6 +2,7 @@ import 'package:json_annotation/json_annotation.dart';
import 'package:pshared/data/dto/describable.dart'; import 'package:pshared/data/dto/describable.dart';
import 'package:pshared/data/dto/ledger/balance.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/status.dart';
import 'package:pshared/data/dto/ledger/type.dart'; import 'package:pshared/data/dto/ledger/type.dart';
@@ -20,7 +21,8 @@ class LedgerAccountDTO {
@JsonKey(fromJson: ledgerAccountStatusFromJson, toJson: ledgerAccountStatusToJson) @JsonKey(fromJson: ledgerAccountStatusFromJson, toJson: ledgerAccountStatusToJson)
final LedgerAccountStatusDTO status; final LedgerAccountStatusDTO status;
final bool allowNegative; final bool allowNegative;
final bool isSettlement; @JsonKey(fromJson: ledgerAccountRoleFromJson, toJson: ledgerAccountRoleToJson)
final LedgerAccountRoleDTO role;
final Map<String, String>? metadata; final Map<String, String>? metadata;
final DateTime? createdAt; final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
@@ -38,7 +40,7 @@ class LedgerAccountDTO {
required this.currency, required this.currency,
required this.status, required this.status,
required this.allowNegative, required this.allowNegative,
required this.isSettlement, required this.role,
this.metadata, this.metadata,
this.createdAt, this.createdAt,
this.updatedAt, this.updatedAt,

View File

@@ -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';
}
}

View File

@@ -1,6 +1,7 @@
import 'package:pshared/data/dto/ledger/account.dart'; import 'package:pshared/data/dto/ledger/account.dart';
import 'package:pshared/data/mapper/describable.dart'; import 'package:pshared/data/mapper/describable.dart';
import 'package:pshared/data/mapper/ledger/balance.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/status.dart';
import 'package:pshared/data/mapper/ledger/type.dart'; import 'package:pshared/data/mapper/ledger/type.dart';
import 'package:pshared/models/describable.dart'; import 'package:pshared/models/describable.dart';
@@ -17,7 +18,7 @@ extension LedgerAccountDTOMapper on LedgerAccountDTO {
currency: currency, currency: currency,
status: status.toDomain(), status: status.toDomain(),
allowNegative: allowNegative, allowNegative: allowNegative,
isSettlement: isSettlement, role: role.toDomain(),
metadata: metadata, metadata: metadata,
createdAt: createdAt, createdAt: createdAt,
updatedAt: updatedAt, updatedAt: updatedAt,
@@ -36,7 +37,7 @@ extension LedgerAccountModelMapper on LedgerAccount {
currency: currency, currency: currency,
status: status.toDTO(), status: status.toDTO(),
allowNegative: allowNegative, allowNegative: allowNegative,
isSettlement: isSettlement, role: role.toDTO(),
metadata: metadata, metadata: metadata,
createdAt: createdAt, createdAt: createdAt,
updatedAt: updatedAt, updatedAt: updatedAt,

View File

@@ -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;
}
}
}

View File

@@ -90,6 +90,8 @@ ChainNetwork chainNetworkFromValue(String? value) {
return ChainNetwork.ethereumMainnet; return ChainNetwork.ethereumMainnet;
case 'arbitrum_one': case 'arbitrum_one':
return ChainNetwork.arbitrumOne; return ChainNetwork.arbitrumOne;
case 'arbitrum_sepolia':
return ChainNetwork.arbitrumSepolia;
case 'tron_mainnet': case 'tron_mainnet':
return ChainNetwork.tronMainnet; return ChainNetwork.tronMainnet;
case 'tron_nile': case 'tron_nile':
@@ -111,6 +113,8 @@ String chainNetworkToValue(ChainNetwork chain) {
return 'tron_mainnet'; return 'tron_mainnet';
case ChainNetwork.tronNile: case ChainNetwork.tronNile:
return 'tron_nile'; return 'tron_nile';
case ChainNetwork.arbitrumSepolia:
return 'arbitrum_sepolia';
case ChainNetwork.unspecified: case ChainNetwork.unspecified:
return 'unspecified'; return 'unspecified';
} }

View File

@@ -46,6 +46,11 @@
"description": "Label for the Arbitrum One network" "description": "Label for the Arbitrum One network"
}, },
"chainNetworkArbitrumSepolia": "Arbitrum Sepolia",
"@chainNetworkArbitrumSepolia": {
"description": "Label for the Arbitrum Sepolia network"
},
"chainNetworkTronMainnet": "Tron Mainnet", "chainNetworkTronMainnet": "Tron Mainnet",
"@chainNetworkTronMainnet": { "@chainNetworkTronMainnet": {
"description": "Label for the Tron mainnet network" "description": "Label for the Tron mainnet network"

View File

@@ -46,6 +46,11 @@
"description": "Label for the Arbitrum One network" "description": "Label for the Arbitrum One network"
}, },
"chainNetworkArbitrumSepolia": "Arbitrum Sepolia",
"@chainNetworkArbitrumSepolia": {
"description": "Label for the Arbitrum Sepolia network"
},
"chainNetworkTronMainnet": "Tron Mainnet", "chainNetworkTronMainnet": "Tron Mainnet",
"@chainNetworkTronMainnet": { "@chainNetworkTronMainnet": {
"description": "Label for the Tron mainnet network" "description": "Label for the Tron mainnet network"

View File

@@ -1,5 +1,6 @@
import 'package:pshared/models/describable.dart'; import 'package:pshared/models/describable.dart';
import 'package:pshared/models/ledger/balance.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/status.dart';
import 'package:pshared/models/ledger/type.dart'; import 'package:pshared/models/ledger/type.dart';
@@ -13,7 +14,7 @@ class LedgerAccount implements Describable {
final String currency; final String currency;
final LedgerAccountStatus status; final LedgerAccountStatus status;
final bool allowNegative; final bool allowNegative;
final bool isSettlement; final LedgerAccountRole role;
final Map<String, String>? metadata; final Map<String, String>? metadata;
final DateTime? createdAt; final DateTime? createdAt;
final DateTime? updatedAt; final DateTime? updatedAt;
@@ -35,7 +36,7 @@ class LedgerAccount implements Describable {
required this.currency, required this.currency,
required this.status, required this.status,
required this.allowNegative, required this.allowNegative,
required this.isSettlement, required this.role,
this.metadata, this.metadata,
this.createdAt, this.createdAt,
this.updatedAt, this.updatedAt,
@@ -55,7 +56,7 @@ class LedgerAccount implements Describable {
currency: currency, currency: currency,
status: status, status: status,
allowNegative: allowNegative, allowNegative: allowNegative,
isSettlement: isSettlement, role: role,
metadata: metadata, metadata: metadata,
createdAt: createdAt, createdAt: createdAt,
updatedAt: updatedAt, updatedAt: updatedAt,

View File

@@ -0,0 +1,14 @@
enum LedgerAccountRole {
unspecified,
operating,
hold,
transit,
settlement,
clearing,
pending,
reserve,
liquidity,
fee,
chargeback,
adjustment,
}

View File

@@ -0,0 +1,6 @@
class CursorPage<T> {
final List<T> items;
final String? nextCursor;
const CursorPage({required this.items, required this.nextCursor});
}

View File

@@ -2,6 +2,7 @@ enum ChainNetwork {
unspecified, unspecified,
ethereumMainnet, ethereumMainnet,
arbitrumOne, arbitrumOne,
arbitrumSepolia,
tronMainnet, tronMainnet,
tronNile tronNile,
} }

View File

@@ -0,0 +1,5 @@
import 'package:pshared/models/pagination/cursor_page.dart';
import 'package:pshared/models/payment/payment.dart';
typedef PaymentPage =CursorPage<Payment>;

View File

@@ -8,6 +8,7 @@ import 'package:collection/collection.dart';
import 'package:pshared/models/currency.dart'; import 'package:pshared/models/currency.dart';
import 'package:pshared/models/describable.dart'; import 'package:pshared/models/describable.dart';
import 'package:pshared/models/ledger/account.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/models/payment/wallet.dart';
import 'package:pshared/provider/organizations.dart'; import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/resource.dart'; import 'package:pshared/provider/resource.dart';
@@ -24,7 +25,7 @@ class LedgerAccountsProvider with ChangeNotifier {
Resource<List<LedgerAccount>> _resource = Resource(data: []); Resource<List<LedgerAccount>> _resource = Resource(data: []);
Resource<List<LedgerAccount>> get resource => _resource; Resource<List<LedgerAccount>> get resource => _resource;
List<LedgerAccount> get accounts => (_resource.data ?? []).whereNot((la)=> la.isSettlement).toList(); List<LedgerAccount> get accounts => (_resource.data ?? []).where((la) => la.role == LedgerAccountRole.operating).toList();
bool get isLoading => _resource.isLoading; bool get isLoading => _resource.isLoading;
Exception? get error => _resource.error; Exception? get error => _resource.error;

View File

@@ -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<List<Payment>> _resource = Resource(data: []);
bool _isLoaded = false;
bool _isLoadingMore = false;
String? _nextCursor;
int? _limit;
String? _sourceRef;
String? _destinationRef;
List<String>? _states;
int _opSeq = 0;
Resource<List<Payment>> get resource => _resource;
List<Payment> 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<void> refresh({
int? limit,
String? sourceRef,
String? destinationRef,
List<String>? 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<void> 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<Payment>.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<List<Payment>> 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<String>? _normalizeStates(List<String>? 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;
}
}

View File

@@ -5,6 +5,7 @@ import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/payment/quotation/quotation.dart'; import 'package:pshared/provider/payment/quotation/quotation.dart';
import 'package:pshared/provider/resource.dart'; import 'package:pshared/provider/resource.dart';
import 'package:pshared/service/payment/service.dart'; import 'package:pshared/service/payment/service.dart';
import 'package:pshared/utils/exception.dart';
class PaymentProvider extends ChangeNotifier { class PaymentProvider extends ChangeNotifier {
@@ -53,11 +54,7 @@ class PaymentProvider extends ChangeNotifier {
_isLoaded = true; _isLoaded = true;
_setResource(_payment.copyWith(data: response, isLoading: false, error: null)); _setResource(_payment.copyWith(data: response, isLoading: false, error: null));
} catch (e) { } catch (e) {
_setResource(_payment.copyWith( _setResource(_payment.copyWith(data: null, error: toException(e), isLoading: false));
data: null,
error: e is Exception ? e : Exception(e.toString()),
isLoading: false,
));
} }
return _payment.data; return _payment.data;
} }

View File

@@ -1,4 +1,6 @@
import 'package:pshared/controllers/balance_mask/wallets.dart'; 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/currency_pair.dart';
import 'package:pshared/models/payment/customer.dart'; import 'package:pshared/models/payment/customer.dart';
import 'package:pshared/models/payment/fx/intent.dart'; import 'package:pshared/models/payment/fx/intent.dart';
@@ -55,6 +57,10 @@ class QuotationIntentBuilder {
destination: paymentData, destination: paymentData,
source: ManagedWalletPaymentMethod( source: ManagedWalletPaymentMethod(
managedWalletRef: selectedWallet.id, managedWalletRef: selectedWallet.id,
asset: PaymentAsset(
tokenSymbol: selectedWallet.tokenSymbol ?? '',
chain: selectedWallet.network ?? ChainNetwork.unspecified,
)
), ),
fx: fxIntent, fx: fxIntent,
settlementMode: payment.payerCoversFee ? SettlementMode.fixReceived : SettlementMode.fixSource, settlementMode: payment.payerCoversFee ? SettlementMode.fixReceived : SettlementMode.fixSource,

View File

@@ -18,6 +18,7 @@ import 'package:pshared/models/resources.dart';
import 'package:pshared/provider/organizations.dart'; import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/resource.dart'; import 'package:pshared/provider/resource.dart';
import 'package:pshared/service/permissions.dart'; import 'package:pshared/service/permissions.dart';
import 'package:pshared/utils/exception.dart';
class PermissionsProvider extends ChangeNotifier { class PermissionsProvider extends ChangeNotifier {
@@ -43,10 +44,7 @@ class PermissionsProvider extends ChangeNotifier {
await operation(); await operation();
return await load(); return await load();
} catch (e) { } catch (e) {
_userAccess = _userAccess.copyWith( _userAccess = _userAccess.copyWith(error: toException(e), isLoading: false);
error: e is Exception ? e : Exception(e.toString()),
isLoading: false,
);
notifyListeners(); notifyListeners();
return _userAccess.data; return _userAccess.data;
} }

View File

@@ -4,11 +4,13 @@ import 'package:pshared/api/responses/ledger/balance.dart';
import 'package:pshared/data/mapper/describable.dart'; import 'package:pshared/data/mapper/describable.dart';
import 'package:pshared/data/mapper/ledger/account.dart'; import 'package:pshared/data/mapper/ledger/account.dart';
import 'package:pshared/data/mapper/ledger/balance.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/data/mapper/ledger/type.dart';
import 'package:pshared/models/currency.dart'; import 'package:pshared/models/currency.dart';
import 'package:pshared/models/describable.dart'; import 'package:pshared/models/describable.dart';
import 'package:pshared/models/ledger/account.dart'; import 'package:pshared/models/ledger/account.dart';
import 'package:pshared/models/ledger/balance.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/models/ledger/type.dart';
import 'package:pshared/service/authorization/service.dart'; import 'package:pshared/service/authorization/service.dart';
import 'package:pshared/service/services.dart'; import 'package:pshared/service/services.dart';
@@ -49,7 +51,7 @@ class LedgerService {
describable: describable.toDTO(), describable: describable.toDTO(),
ownerRef: ownerRef, ownerRef: ownerRef,
allowNegative: false, allowNegative: false,
isSettlement: false, role: LedgerAccountRole.operating.toDTO(),
accountType: LedgerAccountType.asset.toDTO(), accountType: LedgerAccountType.asset.toDTO(),
currency: currencyCodeToString(currency), currency: currencyCodeToString(currency),
).toJson(), ).toJson(),

View File

@@ -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/payment.dart';
import 'package:pshared/api/responses/payment/payments.dart'; import 'package:pshared/api/responses/payment/payments.dart';
import 'package:pshared/data/mapper/payment/payment_response.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/models/payment/payment.dart';
import 'package:pshared/service/authorization/service.dart'; import 'package:pshared/service/authorization/service.dart';
import 'package:pshared/service/services.dart'; import 'package:pshared/service/services.dart';
import 'package:pshared/utils/http/params.dart';
class PaymentService { class PaymentService {
static final _logger = Logger('service.payment'); static final _logger = Logger('service.payment');
static const String _objectType = Services.payments; static const String _objectType = Services.payments;
static Future<List<Payment>> list( static Future<PaymentPage> listPage(
String organizationRef, { String organizationRef, {
int? limit, int? limit,
String? cursor, String? cursor,
@@ -25,12 +27,6 @@ class PaymentService {
}) async { }) async {
_logger.fine('Listing payments for organization $organizationRef'); _logger.fine('Listing payments for organization $organizationRef');
final queryParams = <String, String>{}; final queryParams = <String, String>{};
if (limit != null) {
queryParams['limit'] = limit.toString();
}
if (cursor != null && cursor.isNotEmpty) {
queryParams['cursor'] = cursor;
}
if (sourceRef != null && sourceRef.isNotEmpty) { if (sourceRef != null && sourceRef.isNotEmpty) {
queryParams['source_ref'] = sourceRef; queryParams['source_ref'] = sourceRef;
} }
@@ -41,12 +37,35 @@ class PaymentService {
queryParams['state'] = states.join(','); queryParams['state'] = states.join(',');
} }
final path = '/$organizationRef'; final url = cursorParamsToUriString(
final url = queryParams.isEmpty path: '/$organizationRef',
? path limit: limit,
: Uri(path: path, queryParameters: queryParams).toString(); cursor: cursor,
queryParams: queryParams,
);
final response = await AuthorizationService.getGETResponse(_objectType, url); 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<Payment>> list(
String organizationRef, {
int? limit,
String? cursor,
String? sourceRef,
String? destinationRef,
List<String>? states,
}) async {
final page = await listPage(
organizationRef,
limit: limit,
cursor: cursor,
sourceRef: sourceRef,
destinationRef: destinationRef,
states: states,
);
return page.items;
} }
static Future<Payment> pay( static Future<Payment> pay(
@@ -68,4 +87,5 @@ class PaymentService {
); );
return PaymentResponse.fromJson(response).payment.toDomain(); return PaymentResponse.fromJson(response).payment.toDomain();
} }
} }

View File

@@ -2,6 +2,7 @@
const String _limitParam = 'limit'; const String _limitParam = 'limit';
const String _offsetParam = 'offset'; const String _offsetParam = 'offset';
const String _archivedParam = 'archived'; const String _archivedParam = 'archived';
const String _cursorParam = 'cursor';
void _addIfNotNull(Map<String, String> params, String key, dynamic value) { void _addIfNotNull(Map<String, String> params, String key, dynamic value) {
if (value != null) { if (value != null) {
@@ -9,6 +10,13 @@ void _addIfNotNull(Map<String, String> params, String key, dynamic value) {
} }
} }
void _addIfNotBlank(Map<String, String> params, String key, String? value) {
final trimmed = value?.trim();
if (trimmed != null && trimmed.isNotEmpty) {
params[key] = trimmed;
}
}
Uri paramsToUri({ Uri paramsToUri({
required String path, required String path,
int? limit, int? limit,
@@ -36,3 +44,32 @@ String paramsToUriString({
int? offset, int? offset,
bool? fetchArchived, bool? fetchArchived,
}) => paramsToUri(path: path, limit: limit, offset: offset, fetchArchived: fetchArchived).toString(); }) => paramsToUri(path: path, limit: limit, offset: offset, fetchArchived: fetchArchived).toString();
Uri cursorParamsToUri({
required String path,
int? limit,
String? cursor,
Map<String, String> queryParams = const {},
}) {
final params = Map<String, String>.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<String, String> queryParams = const {},
}) => cursorParamsToUri(
path: path,
limit: limit,
cursor: cursor,
queryParams: queryParams,
).toString();

View File

@@ -15,6 +15,8 @@ extension ChainNetworkL10n on ChainNetwork {
return l10n.chainNetworkEthereumMainnet; return l10n.chainNetworkEthereumMainnet;
case ChainNetwork.arbitrumOne: case ChainNetwork.arbitrumOne:
return l10n.chainNetworkArbitrumOne; return l10n.chainNetworkArbitrumOne;
case ChainNetwork.arbitrumSepolia:
return l10n.chainNetworkArbitrumSepolia;
case ChainNetwork.tronMainnet: case ChainNetwork.tronMainnet:
return l10n.chainNetworkTronMainnet; return l10n.chainNetworkTronMainnet;
case ChainNetwork.tronNile: case ChainNetwork.tronNile:

View File

@@ -20,6 +20,7 @@ import 'package:pshared/provider/recipient/provider.dart';
import 'package:pshared/provider/email_verification.dart'; import 'package:pshared/provider/email_verification.dart';
import 'package:pshared/provider/ledger.dart'; import 'package:pshared/provider/ledger.dart';
import 'package:pshared/provider/payment/wallets.dart'; import 'package:pshared/provider/payment/wallets.dart';
import 'package:pshared/provider/payment/payments.dart';
import 'package:pshared/provider/invitations.dart'; import 'package:pshared/provider/invitations.dart';
import 'package:pshared/service/ledger.dart'; import 'package:pshared/service/ledger.dart';
import 'package:pshared/service/payment/wallets.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/app/app.dart';
import 'package:pweb/pages/invitations/widgets/list/view_model.dart'; import 'package:pweb/pages/invitations/widgets/list/view_model.dart';
import 'package:pweb/app/timeago.dart'; import 'package:pweb/app/timeago.dart';
import 'package:pweb/providers/operatioins.dart';
import 'package:pweb/providers/two_factor.dart'; import 'package:pweb/providers/two_factor.dart';
import 'package:pweb/providers/upload_history.dart'; import 'package:pweb/providers/upload_history.dart';
import 'package:pweb/providers/wallet_transactions.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/payments/history.dart';
import 'package:pweb/services/posthog.dart'; import 'package:pweb/services/posthog.dart';
import 'package:pweb/services/wallet_transactions.dart'; import 'package:pweb/services/wallet_transactions.dart';
@@ -78,6 +77,10 @@ void main() async {
create: (_) => EmployeesProvider(), create: (_) => EmployeesProvider(),
update: (context, organizations, provider) => provider!..updateProviders(organizations), update: (context, organizations, provider) => provider!..updateProviders(organizations),
), ),
ChangeNotifierProxyProvider<OrganizationsProvider, PaymentsProvider>(
create: (_) => PaymentsProvider(),
update: (context, organizations, provider) => provider!..update(organizations),
),
ChangeNotifierProvider(create: (_) => EmailVerificationProvider()), ChangeNotifierProvider(create: (_) => EmailVerificationProvider()),
ChangeNotifierProvider( ChangeNotifierProvider(
@@ -117,9 +120,6 @@ void main() async {
ChangeNotifierProvider( ChangeNotifierProvider(
create: (_) => WalletTransactionsProvider(MockWalletTransactionsService())..load(), create: (_) => WalletTransactionsProvider(MockWalletTransactionsService())..load(),
), ),
ChangeNotifierProvider(
create: (_) => OperationProvider(OperationService())..loadOperations(),
),
], ],
child: const PayApp(), child: const PayApp(),
), ),

View File

@@ -2,11 +2,15 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.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/distribution.dart';
import 'package:pweb/pages/report/charts/status.dart'; import 'package:pweb/pages/report/charts/status.dart';
import 'package:pweb/pages/report/table/filters.dart'; import 'package:pweb/pages/report/table/filters.dart';
import 'package:pweb/pages/report/table/widget.dart'; import 'package:pweb/pages/report/table/widget.dart';
import 'package:pweb/providers/operatioins.dart';
import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -19,18 +23,25 @@ class OperationHistoryPage extends StatefulWidget {
} }
class _OperationHistoryPageState extends State<OperationHistoryPage> { class _OperationHistoryPageState extends State<OperationHistoryPage> {
DateTimeRange? _pendingRange;
DateTimeRange? _appliedRange;
final Set<OperationStatus> _pendingStatuses = {};
Set<OperationStatus> _appliedStatuses = {};
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<OperationProvider>().loadOperations(); final provider = context.read<PaymentsProvider>();
if (!provider.isReady && !provider.isLoading) {
provider.refresh();
}
}); });
} }
Future<void> _pickRange() async { Future<void> _pickRange() async {
final provider = context.read<OperationProvider>();
final now = DateTime.now(); final now = DateTime.now();
final initial = provider.dateRange ?? final initial = _pendingRange ??
DateTimeRange( DateTimeRange(
start: now.subtract(const Duration(days: 30)), start: now.subtract(const Duration(days: 30)),
end: now, end: now,
@@ -44,33 +55,157 @@ class _OperationHistoryPageState extends State<OperationHistoryPage> {
); );
if (picked != null) { 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<OperationItem> _mapPayments(List<Payment> 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<OperationItem> _filterOperations(List<OperationItem> 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<String?> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!; final loc = AppLocalizations.of(context)!;
return Consumer<OperationProvider>( return Consumer<PaymentsProvider>(
builder: (context, provider, child) { builder: (context, provider, child) {
if (provider.isLoading) { if (provider.isLoading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
if (provider.error != null) { if (provider.error != null) {
final message = provider.error?.toString() ?? loc.noErrorInformation;
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text(loc.notificationError(provider.error ?? loc.noErrorInformation)), Text(loc.notificationError(message)),
ElevatedButton( ElevatedButton(
onPressed: () => provider.loadOperations(), onPressed: () => provider.refresh(),
child: Text(loc.retry), child: Text(loc.retry),
), ),
], ],
), ),
); );
} }
final operations = _mapPayments(provider.payments);
final filteredOperations = _filterOperations(operations);
final hasFileName = operations.any(
(operation) => (operation.fileName ?? '').trim().isNotEmpty,
);
return Padding( return Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
@@ -84,26 +219,26 @@ class _OperationHistoryPageState extends State<OperationHistoryPage> {
spacing: 16, spacing: 16,
children: [ children: [
Expanded( Expanded(
child: StatusChart(operations: provider.allOperations), child: StatusChart(operations: operations),
), ),
Expanded( Expanded(
child: PayoutDistributionChart( child: PayoutDistributionChart(
operations: provider.allOperations, operations: operations,
), ),
), ),
], ],
), ),
), ),
OperationFilters( OperationFilters(
selectedRange: provider.dateRange, selectedRange: _pendingRange,
selectedStatuses: provider.selectedStatuses, selectedStatuses: _pendingStatuses,
onPickRange: _pickRange, onPickRange: _pickRange,
onToggleStatus: provider.toggleStatus, onToggleStatus: _toggleStatus,
onApply: () => provider.applyFilters(context), onApply: _applyFilters,
), ),
OperationsTable( OperationsTable(
operations: provider.filteredOperations, operations: filteredOperations,
showFileNameColumn: provider.hasFileName, showFileNameColumn: hasFileName,
), ),
], ],
), ),

View File

@@ -6,10 +6,10 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class OperationFilters extends StatelessWidget { class OperationFilters extends StatelessWidget {
final DateTimeRange? selectedRange; final DateTimeRange? selectedRange;
final Set<String> selectedStatuses; final Set<OperationStatus> selectedStatuses;
final VoidCallback onPickRange; final VoidCallback onPickRange;
final VoidCallback onApply; final VoidCallback onApply;
final ValueChanged<String> onToggleStatus; final ValueChanged<OperationStatus> onToggleStatus;
const OperationFilters({ const OperationFilters({
super.key, super.key,
@@ -66,11 +66,12 @@ class OperationFilters extends StatelessWidget {
Wrap( Wrap(
spacing: 12, spacing: 12,
runSpacing: 8, runSpacing: 8,
children: [ children: const [
OperationStatus.success.localized(context), OperationStatus.success,
OperationStatus.processing.localized(context), OperationStatus.processing,
OperationStatus.error.localized(context), OperationStatus.error,
].map((status) { ].map((status) {
final label = status.localized(context);
final isSelected = selectedStatuses.contains(status); final isSelected = selectedStatuses.contains(status);
return GestureDetector( return GestureDetector(
onTap: () => onToggleStatus(status), onTap: () => onToggleStatus(status),
@@ -89,7 +90,7 @@ class OperationFilters extends StatelessWidget {
vertical: 4, vertical: 4,
), ),
child: Text( child: Text(
l10n.status(status), l10n.status(label),
style: TextStyle( style: TextStyle(
color: isSelected ? Colors.white : Colors.black87, color: isSelected ? Colors.white : Colors.black87,
fontSize: 14, fontSize: 14,

View File

@@ -8,6 +8,13 @@ import 'package:pweb/pages/report/table/badge.dart';
class OperationRow { class OperationRow {
static DataRow build(OperationItem op, BuildContext context) { 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: [ return DataRow(cells: [
DataCell(OperationStatusBadge(status: op.status)), DataCell(OperationStatusBadge(status: op.status)),
DataCell(Text(op.fileName ?? '')), DataCell(Text(op.fileName ?? '')),
@@ -16,10 +23,7 @@ class OperationRow {
DataCell(Text(op.payId)), DataCell(Text(op.payId)),
DataCell(Text(op.cardNumber ?? '-')), DataCell(Text(op.cardNumber ?? '-')),
DataCell(Text(op.name)), DataCell(Text(op.name)),
DataCell(Text( DataCell(Text(dateLabel)),
'${TimeOfDay.fromDateTime(op.date).format(context)}\n'
'${op.date.toLocal().toIso8601String().split("T").first}',
)),
DataCell(Text(op.comment)), DataCell(Text(op.comment)),
]); ]);
} }