reports page

This commit is contained in:
Arseni
2026-02-16 21:05:38 +03:00
parent 11d4b9a608
commit 0eea39fb97
56 changed files with 2227 additions and 501 deletions

View File

@@ -0,0 +1,22 @@
AmountParts splitAmount(String value) {
final trimmed = value.trim();
if (trimmed.isEmpty || trimmed == '-') {
return const AmountParts(amount: '-', currency: '');
}
final parts = trimmed.split(' ');
if (parts.length < 2) {
return AmountParts(amount: trimmed, currency: '');
}
final currency = parts.removeLast();
return AmountParts(amount: parts.join(' '), currency: currency);
}
class AmountParts {
final String amount;
final String currency;
const AmountParts({
required this.amount,
required this.currency,
});
}

View File

@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/provider/organizations.dart';
import 'package:pshared/service/payment/documents.dart';
import 'package:pweb/utils/download.dart';
import 'package:pweb/utils/error/snackbar.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
Future<void> downloadPaymentAct(BuildContext context, String paymentRef) async {
final organizations = context.read<OrganizationsProvider>();
if (!organizations.isOrganizationSet) {
return;
}
final trimmed = paymentRef.trim();
if (trimmed.isEmpty) {
return;
}
final loc = AppLocalizations.of(context)!;
await executeActionWithNotification(
context: context,
action: () async {
final file = await PaymentDocumentsService.getAct(
organizations.current.id,
trimmed,
);
await downloadFile(file);
},
errorMessage: loc.downloadActError,
);
}

View File

@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:pshared/models/money.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/localization.dart';
String formatMoney(Money? money, {String fallback = '-'}) {
if (money == null) return fallback;
final amount = money.amount.trim();
if (amount.isEmpty) return fallback;
final symbol = currencySymbolFromCode(money.currency);
final suffix = symbol ?? money.currency;
if (suffix.trim().isEmpty) return amount;
return '$amount $suffix';
}
String formatAmount(double amount, String currency, {String fallback = '-'}) {
final trimmed = currency.trim();
if (trimmed.isEmpty) return amountToString(amount);
final symbol = currencySymbolFromCode(trimmed);
final suffix = symbol ?? trimmed;
return '${amountToString(amount)} $suffix';
}
String formatDateLabel(BuildContext context, DateTime? date, {String fallback = '-'}) {
if (date == null || date.millisecondsSinceEpoch == 0) return fallback;
return dateTimeToLocalFormat(context, date.toLocal());
}
String formatLongDate(BuildContext context, DateTime? date, {String fallback = '-'}) {
if (date == null || date.millisecondsSinceEpoch == 0) return fallback;
final locale = Localizations.localeOf(context).toString();
final formatter = DateFormat('d MMMM y', locale);
return formatter.format(date.toLocal());
}
String collapseWhitespace(String value) {
return value.replaceAll(RegExp(r'\s+'), ' ').trim();
}

View File

@@ -0,0 +1,23 @@
import 'package:pshared/models/payment/operation.dart';
List<OperationItem> sortOperations(List<OperationItem> operations) {
final sorted = List<OperationItem>.from(operations);
sorted.sort((a, b) {
final aTime = a.date.millisecondsSinceEpoch;
final bTime = b.date.millisecondsSinceEpoch;
final aUnknown = isUnknownDate(a.date);
final bUnknown = isUnknownDate(b.date);
if (aUnknown != bUnknown) {
return aUnknown ? 1 : -1;
}
if (aTime != bTime) {
return bTime.compareTo(aTime);
}
return a.payId.compareTo(b.payId);
});
return sorted;
}
bool isUnknownDate(DateTime date) => date.millisecondsSinceEpoch == 0;

View File

@@ -0,0 +1,117 @@
import 'package:pshared/models/payment/operation.dart';
import 'package:pshared/models/payment/payment.dart';
import 'package:pshared/models/payment/status.dart';
OperationItem mapPaymentToOperation(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: statusFromPayment(payment),
fileName: _extractFileName(payment.metadata),
amount: amount,
currency: currency,
toAmount: toAmount,
toCurrency: toCurrency,
payId: payId,
paymentRef: payment.paymentRef,
cardNumber: null,
name: name,
date: resolvePaymentDate(payment),
comment: comment,
);
}
OperationStatus statusFromPayment(Payment payment) {
final normalized = _normalizePaymentState(payment.state);
switch (normalized) {
case 'SUCCESS':
return OperationStatus.success;
case 'FAILED':
case 'CANCELLED':
return OperationStatus.error;
default:
return OperationStatus.processing;
}
}
DateTime resolvePaymentDate(Payment payment) {
final createdAt = payment.createdAt;
if (createdAt != null) return createdAt.toLocal();
final expiresAt = payment.lastQuote?.fxQuote?.expiresAtUnixMs;
if (expiresAt != null && expiresAt > 0) {
return DateTime.fromMillisecondsSinceEpoch(expiresAt, isUtc: true).toLocal();
}
return DateTime.fromMillisecondsSinceEpoch(0);
}
String? paymentIdFromOperation(OperationItem operation) {
final candidates = [
operation.paymentRef,
operation.payId,
];
for (final candidate in candidates) {
final trimmed = candidate?.trim();
if (trimmed != null && trimmed.isNotEmpty && trimmed != '-') {
return trimmed;
}
}
return null;
}
String? _extractFileName(Map<String, String>? metadata) {
if (metadata == null || metadata.isEmpty) return null;
return _firstNonEmpty([
metadata['upload_filename'],
metadata['upload_file_name'],
metadata['filename'],
]);
}
String? _firstNonEmpty(List<String?> values) {
for (final value in values) {
final trimmed = value?.trim();
if (trimmed != null && trimmed.isNotEmpty) return trimmed;
}
return null;
}
String _normalizePaymentState(String? raw) {
final trimmed = (raw ?? '').trim().toUpperCase();
if (trimmed.startsWith('PAYMENT_STATE_')) {
return trimmed.substring('PAYMENT_STATE_'.length);
}
return trimmed;
}
double _parseAmount(String? amount) {
if (amount == null || amount.trim().isEmpty) return 0;
return double.tryParse(amount) ?? 0;
}