Frontend first draft
This commit is contained in:
103
frontend/pweb/lib/pages/report/charts/distribution.dart
Normal file
103
frontend/pweb/lib/pages/report/charts/distribution.dart
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user