Frontend first draft

This commit is contained in:
Arseni
2025-11-13 15:06:15 +03:00
parent e47f343afb
commit ddb54ddfdc
504 changed files with 25498 additions and 1 deletions

View File

@@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
import 'package:pshared/models/payment/operation.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PayoutDistributionChart extends StatelessWidget {
final List<OperationItem> operations;
const PayoutDistributionChart({super.key, required this.operations});
@override
Widget build(BuildContext context) {
// 1) Aggregate sums
final sums = <String, double>{};
for (var op in operations) {
final name = op.name ?? AppLocalizations.of(context)!.unknown;
sums[name] = (sums[name] ?? 0) + op.amount;
}
if (sums.isEmpty) {
return Center(child: Text(AppLocalizations.of(context)!.noPayouts));
}
// 2) Build chart data
final data = sums.entries
.map((e) => _ChartData(e.key, e.value))
.toList();
// 3) Build a simple horizontal legend
final palette = [
Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.secondary,
Theme.of(context).colorScheme.tertiary ?? Colors.grey,
Theme.of(context).colorScheme.primaryContainer,
Theme.of(context).colorScheme.secondaryContainer,
];
final legendItems = List<Widget>.generate(data.length, (i) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.circle, size: 10, color: palette[i % palette.length]),
const SizedBox(width: 4),
Text(data[i].label, style: Theme.of(context).textTheme.bodySmall),
if (i < data.length - 1) const SizedBox(width: 12),
],
);
});
return Card(
margin: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// Pie takes 2/3 of the width
Expanded(
flex: 2,
child: SfCircularChart(
legend: Legend(isVisible: false),
tooltipBehavior: TooltipBehavior(enable: true),
series: <PieSeries<_ChartData, String>>[
PieSeries<_ChartData, String>(
dataSource: data,
xValueMapper: (d, _) => d.label,
yValueMapper: (d, _) => d.value,
dataLabelMapper: (d, _) =>
'${(d.value / sums.values.fold(0, (a, b) => a + b) * 100).toStringAsFixed(1)}%',
dataLabelSettings: const DataLabelSettings(
isVisible: true,
labelPosition: ChartDataLabelPosition.inside,
),
radius: '100%',
)
],
),
),
const SizedBox(width: 16),
// Legend takes 1/3
Expanded(
flex: 1,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Column(spacing: 4.0, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: legendItems),
),
),
],
),
),
);
}
}
class _ChartData {
final String label;
final double value;
_ChartData(this.label, this.value);
}

View File

@@ -0,0 +1,91 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
import 'package:pshared/models/payment/status.dart';
import 'package:pshared/models/payment/operation.dart';
class StatusChart extends StatelessWidget {
final List<OperationItem> operations;
const StatusChart({super.key, required this.operations});
@override
Widget build(BuildContext context) {
// 1) Compute counts
final counts = <OperationStatus, int>{};
for (var op in operations) {
counts[op.status] = (counts[op.status] ?? 0) + 1;
}
final items = counts.entries
.map((e) => _ChartData(e.key, e.value.toDouble()))
.toList();
final maxCount = items.map((e) => e.count.toInt()).fold<int>(0, max);
final theme = Theme.of(context);
final barColor = theme.colorScheme.secondary;
final caption = theme.textTheme.labelMedium;
return SizedBox(
height: 200,
child: Card(
margin: const EdgeInsets.all(16),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8),
child: SfCartesianChart(
// ─── Axes ─────────────────────────────────────────
primaryXAxis: CategoryAxis(
labelStyle: caption,
majorGridLines: const MajorGridLines(width: 0),
),
primaryYAxis: NumericAxis(
minimum: 0,
maximum: (maxCount + 1).toDouble(),
interval: 1,
labelStyle: caption,
majorGridLines: MajorGridLines(
color: theme.dividerColor.withAlpha(76),
width: 1,
dashArray: <double>[4, 2],
),
),
// ─── Enable tooltips ───────────────────────────────
legend: Legend(isVisible: false),
tooltipBehavior: TooltipBehavior(
enable: true,
header: '', // omit series name in header
format: 'point.x : point.y', // e.g. "Init : 2"
),
// ─── Bar series with tooltip enabled ───────────────
series: <ColumnSeries<_ChartData, String>>[
ColumnSeries<_ChartData, String>(
dataSource: items,
xValueMapper: (d, _) => d.status.localized(context),
yValueMapper: (d, _) => d.count,
color: barColor,
width: 0.6,
borderRadius: const BorderRadius.all(Radius.circular(4)),
enableTooltip: true, // <— turn on for this series
),
],
),
),
),
);
}
}
class _ChartData {
final OperationStatus status;
final double count;
_ChartData(this.status, this.count);
}