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

@@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:pweb/app/router/payout_routes.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -7,21 +10,30 @@ class UploadHistoryHeader extends StatelessWidget {
const UploadHistoryHeader({
super.key,
required this.theme,
required this.l10n,
});
final ThemeData theme;
final AppLocalizations l10n;
static const double _smallBox = 5;
@override
Widget build(BuildContext context) {
final l10 = AppLocalizations.of(context)!;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Icon(Icons.history),
const SizedBox(width: _smallBox),
Text(l10n.uploadHistory, style: theme.textTheme.bodyLarge),
Row(
children: [
const Icon(Icons.history),
const SizedBox(width: _smallBox),
Text(l10.uploadHistory, style: theme.textTheme.bodyLarge),
],
),
TextButton.icon(
onPressed: () => context.goToPayout(PayoutDestination.reports),
icon: const Icon(Icons.open_in_new, size: 16),
label: Text(l10.viewWholeHistory),
),
],
);
}

View File

@@ -1,40 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class StatusView {
final String label;
final Color color;
const StatusView(this.label, this.color);
}
StatusView statusView(AppLocalizations l10n, String? raw) {
final trimmed = (raw ?? '').trim();
final upper = trimmed.toUpperCase();
final normalized = upper.startsWith('PAYMENT_STATE_')
? upper.substring('PAYMENT_STATE_'.length)
: upper;
switch (normalized) {
case 'SETTLED':
return StatusView(l10n.paymentStatusPending, Colors.yellow);
case 'SUCCESS':
return StatusView(l10n.paymentStatusSuccessful, Colors.green);
case 'FUNDS_RESERVED':
return StatusView(l10n.paymentStatusReserved, Colors.blue);
case 'ACCEPTED':
return StatusView(l10n.paymentStatusProcessing, Colors.yellow);
case 'SUBMITTED':
return StatusView(l10n.paymentStatusProcessing, Colors.blue);
case 'FAILED':
return StatusView(l10n.paymentStatusFailed, Colors.red);
case 'CANCELLED':
return StatusView(l10n.paymentStatusCancelled, Colors.grey);
case 'UNSPECIFIED':
case '':
default:
return StatusView(l10n.paymentStatusPending, Colors.grey);
}
}

View File

@@ -1,37 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/sections/history/helpers.dart';
class HistoryStatusBadge extends StatelessWidget {
const HistoryStatusBadge({
super.key,
required this.statusView,
});
final StatusView statusView;
static const double _radius = 6;
static const double _statusBgOpacity = 0.12;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: statusView.color.withValues(alpha: _statusBgOpacity),
borderRadius: BorderRadius.circular(_radius),
),
child: Text(
statusView.label,
style: TextStyle(
color: statusView.color,
fontWeight: FontWeight.w600,
),
),
);
}
}

View File

@@ -1,68 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:pshared/models/payment/payment.dart';
import 'package:pweb/controllers/upload_history_table.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/sections/history/helpers.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/sections/history/status_badge.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class UploadHistoryTable extends StatelessWidget {
const UploadHistoryTable({
super.key,
required this.items,
required this.dateFormat,
required this.l10n,
});
final List<Payment> items;
final DateFormat dateFormat;
final AppLocalizations l10n;
static const int _maxVisibleItems = 10;
static const UploadHistoryTableController _controller =
UploadHistoryTableController();
@override
Widget build(BuildContext context) {
final visibleItems = items.take(_maxVisibleItems).toList(growable: false);
return DataTable(
columns: [
DataColumn(label: Text(l10n.fileNameColumn)),
DataColumn(label: Text(l10n.rowsColumn)),
DataColumn(label: Text(l10n.dateColumn)),
DataColumn(label: Text(l10n.amountColumn)),
DataColumn(label: Text(l10n.statusColumn)),
],
rows: visibleItems.map((payment) {
final metadata = payment.metadata;
final status = statusView(l10n, payment.state);
final fileName = metadata?['upload_filename'];
final fileNameText =
(fileName == null || fileName.isEmpty) ? '-' : fileName;
final rows = metadata?['upload_rows'];
final rowsText = (rows == null || rows.isEmpty) ? '-' : rows;
final createdAt = payment.createdAt;
final dateText = createdAt == null
? '-'
: dateFormat.format(createdAt.toLocal());
final amountText = _controller.amountText(payment);
return DataRow(
cells: [
DataCell(Text(fileNameText)),
DataCell(Text(rowsText)),
DataCell(Text(dateText)),
DataCell(Text(amountText)),
DataCell(HistoryStatusBadge(statusView: status)),
],
);
}).toList(),
);
}
}

View File

@@ -1,13 +1,15 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/payment/operation.dart';
import 'package:pshared/provider/payment/payments.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/sections/history/header.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/sections/history/table.dart';
import 'package:pweb/controllers/recent_payments.dart';
import 'package:pweb/pages/report/cards/column.dart';
import 'package:pweb/utils/report/payment_mapper.dart';
import 'package:pweb/app/router/payout_routes.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -17,61 +19,49 @@ class UploadHistorySection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final provider = context.watch<PaymentsProvider>();
final theme = Theme.of(context);
final l10 = AppLocalizations.of(context)!;
final dateFormat = DateFormat.yMMMd().add_Hm();
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (provider.error != null) {
return Text(
l10.notificationError(provider.error ?? l10.noErrorInformation),
);
}
final items = List.of(provider.payments);
items.sort((a, b) {
final left = a.createdAt;
final right = b.createdAt;
if (left == null && right == null) return 0;
if (left == null) return 1;
if (right == null) return -1;
return right.compareTo(left);
});
return Column(
children: [
UploadHistoryHeader(theme: theme, l10n: l10),
const SizedBox(height: 8),
if (items.isEmpty)
Align(
alignment: Alignment.centerLeft,
child: Text(
l10.walletHistoryEmpty,
style: theme.textTheme.bodyMedium,
),
)
else ...[
UploadHistoryTable(
items: items,
dateFormat: dateFormat,
l10n: l10,
),
//TODO redirect to Reports page
// if (hasMore) ...[
// const SizedBox(height: 8),
// Align(
// alignment: Alignment.centerLeft,
// child: TextButton.icon(
// onPressed: () => context.goNamed(PayoutRoutes.reports),
// icon: const Icon(Icons.open_in_new, size: 16),
// label: Text(l10.viewWholeHistory),
// ),
// ),
// ],
],
],
return ChangeNotifierProxyProvider<PaymentsProvider, RecentPaymentsController>(
create: (_) => RecentPaymentsController(),
update: (_, payments, controller) => controller!..update(payments),
child: const _RecentPaymentsView(),
);
}
}
class _RecentPaymentsView extends StatelessWidget {
const _RecentPaymentsView();
void _openPaymentDetails(BuildContext context, OperationItem operation) {
final paymentId = paymentIdFromOperation(operation);
if (paymentId == null) return;
context.pushToReportPayment(paymentId);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final l10 = AppLocalizations.of(context)!;
return Consumer<RecentPaymentsController>(
builder: (context, controller, child) {
if (controller.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (controller.error != null) {
return Text(
l10.notificationError(controller.error ?? l10.noErrorInformation),
);
}
return Column(
children: [
UploadHistoryHeader(theme: theme),
const SizedBox(height: 8),
OperationsCardsColumn(
operations: controller.recentOperations,
onTap: (operation) => _openPaymentDetails(context, operation),
),
],
);
},
);
}
}