reports page
This commit is contained in:
43
frontend/pweb/lib/pages/report/cards/column.dart
Normal file
43
frontend/pweb/lib/pages/report/cards/column.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/operation.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
import 'package:pweb/pages/report/cards/items.dart';
|
||||
|
||||
|
||||
class OperationsCardsColumn extends StatelessWidget {
|
||||
final List<OperationItem> operations;
|
||||
final ValueChanged<OperationItem>? onTap;
|
||||
|
||||
const OperationsCardsColumn({
|
||||
super.key,
|
||||
required this.operations,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final theme = Theme.of(context);
|
||||
final items = buildOperationCardItems(
|
||||
context,
|
||||
operations,
|
||||
onTap: onTap,
|
||||
);
|
||||
|
||||
if (operations.isEmpty) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
loc.reportPaymentsEmpty,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: items,
|
||||
);
|
||||
}
|
||||
}
|
||||
74
frontend/pweb/lib/pages/report/cards/items.dart
Normal file
74
frontend/pweb/lib/pages/report/cards/items.dart
Normal file
@@ -0,0 +1,74 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/operation.dart';
|
||||
|
||||
import 'package:pweb/pages/report/cards/operation_card.dart';
|
||||
import 'package:pweb/utils/report/format.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
List<Widget> buildOperationCardItems(
|
||||
BuildContext context,
|
||||
List<OperationItem> operations, {
|
||||
ValueChanged<OperationItem>? onTap,
|
||||
}) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final items = <Widget>[];
|
||||
String? currentKey;
|
||||
|
||||
for (final operation in operations) {
|
||||
final dateKey = _dateKey(operation.date);
|
||||
if (dateKey != currentKey) {
|
||||
if (items.isNotEmpty) {
|
||||
items.add(const SizedBox(height: 16));
|
||||
}
|
||||
items.add(_DateHeader(
|
||||
label: _dateLabel(context, operation.date, loc),
|
||||
));
|
||||
items.add(const SizedBox(height: 8));
|
||||
currentKey = dateKey;
|
||||
}
|
||||
|
||||
items.add(OperationCard(
|
||||
operation: operation,
|
||||
onTap: onTap,
|
||||
));
|
||||
items.add(const SizedBox(height: 12));
|
||||
}
|
||||
|
||||
if (items.isNotEmpty) {
|
||||
items.removeLast();
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
String _dateKey(DateTime date) {
|
||||
if (date.millisecondsSinceEpoch == 0) return 'unknown';
|
||||
final local = date.toLocal();
|
||||
final normalized = DateTime(local.year, local.month, local.day);
|
||||
return normalized.toIso8601String();
|
||||
}
|
||||
|
||||
String _dateLabel(BuildContext context, DateTime date, AppLocalizations loc) {
|
||||
if (date.millisecondsSinceEpoch == 0) return loc.unknown;
|
||||
return formatLongDate(context, date);
|
||||
}
|
||||
|
||||
class _DateHeader extends StatelessWidget {
|
||||
final String label;
|
||||
|
||||
const _DateHeader({required this.label});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Text(
|
||||
label,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
44
frontend/pweb/lib/pages/report/cards/list.dart
Normal file
44
frontend/pweb/lib/pages/report/cards/list.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/operation.dart';
|
||||
|
||||
import 'package:pweb/pages/report/cards/items.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class OperationsCardsList extends StatelessWidget {
|
||||
final List<OperationItem> operations;
|
||||
final ValueChanged<OperationItem>? onTap;
|
||||
|
||||
const OperationsCardsList({
|
||||
super.key,
|
||||
required this.operations,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final items = buildOperationCardItems(
|
||||
context,
|
||||
operations,
|
||||
onTap: onTap,
|
||||
);
|
||||
|
||||
return Expanded(
|
||||
child: operations.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
loc.reportPaymentsEmpty,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) => items[index],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
87
frontend/pweb/lib/pages/report/cards/operation_card.dart
Normal file
87
frontend/pweb/lib/pages/report/cards/operation_card.dart
Normal file
@@ -0,0 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/operation.dart';
|
||||
|
||||
import 'package:pweb/pages/report/cards/operation_card_utils.dart';
|
||||
import 'package:pweb/pages/report/cards/operation_info_row.dart';
|
||||
import 'package:pweb/pages/report/table/badge.dart';
|
||||
import 'package:pweb/utils/report/format.dart';
|
||||
import 'package:pweb/utils/report/payment_mapper.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class OperationCard extends StatelessWidget {
|
||||
final OperationItem operation;
|
||||
final ValueChanged<OperationItem>? onTap;
|
||||
|
||||
const OperationCard({
|
||||
super.key,
|
||||
required this.operation,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final theme = Theme.of(context);
|
||||
final canOpen = onTap != null && paymentIdFromOperation(operation) != null;
|
||||
final amountLabel = formatAmount(operation.amount, operation.currency);
|
||||
final toAmountLabel = formatAmount(operation.toAmount, operation.toCurrency);
|
||||
final showToAmount = shouldShowToAmount(operation);
|
||||
final timeLabel = formatOperationTime(context, operation.date);
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(color: theme.dividerColor.withAlpha(25)),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: canOpen ? () => onTap?.call(operation) : null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
OperationStatusBadge(status: operation.status),
|
||||
const Spacer(),
|
||||
Text(
|
||||
timeLabel,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
amountLabel,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (showToAmount)
|
||||
Text(
|
||||
loc.recipientWillReceive(toAmountLabel),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if ((operation.fileName ?? '').trim().isNotEmpty) ...[
|
||||
const SizedBox(height: 6),
|
||||
OperationInfoRow(
|
||||
icon: Icons.description,
|
||||
value: operation.fileName!,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/operation.dart';
|
||||
|
||||
|
||||
bool shouldShowToAmount(OperationItem operation) {
|
||||
if (operation.toCurrency.trim().isEmpty) return false;
|
||||
if (operation.currency.trim().isEmpty) return true;
|
||||
if (operation.currency != operation.toCurrency) return true;
|
||||
return (operation.toAmount - operation.amount).abs() > 0.0001;
|
||||
}
|
||||
|
||||
String formatOperationTime(BuildContext context, DateTime date) {
|
||||
if (date.millisecondsSinceEpoch == 0) return '-';
|
||||
return TimeOfDay.fromDateTime(date.toLocal()).format(context);
|
||||
}
|
||||
36
frontend/pweb/lib/pages/report/cards/operation_info_row.dart
Normal file
36
frontend/pweb/lib/pages/report/cards/operation_info_row.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class OperationInfoRow extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String value;
|
||||
|
||||
const OperationInfoRow({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user