basic csv loader, transaction list & half done stats page

This commit is contained in:
2024-02-04 22:34:28 +01:00
commit 3abee9ff6f
179 changed files with 6999 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:tunas/domains/charts/chart_bloc.dart';
import 'package:tunas/pages/stats/widgets/account_counters.dart';
import 'package:tunas/pages/stats/widgets/categories_totals_chart.dart';
import 'package:tunas/pages/stats/widgets/main_counter.dart';
import 'package:tunas/pages/stats/widgets/monthly_categories_total_chart.dart';
import 'package:tunas/pages/stats/widgets/monthly_total_chart.dart';
import 'package:tunas/pages/stats/widgets/profit_indicator.dart';
import 'package:tunas/pages/stats/widgets/year_selector.dart';
import 'package:tunas/repositories/account/account_repository.dart';
class StatsPage extends StatelessWidget {
const StatsPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => ChartBloc(accountRepository: RepositoryProvider.of<AccountRepository>(context)),
child: BlocBuilder<ChartBloc, ChartState>(
builder: (context, state) => ListView(
children: [
Row(
children: [
Expanded(
flex: 2,
child: MainCounter(value: state.globalTotal)
),
Expanded(
flex: 1,
child: AccountCounter(accountsTotals: state.accountsTotals)
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const YearSelector(),
ProfitIndicator(monthlyTotals: state.scopedMonthlyTotals)
],
),
SizedBox(
height: 200,
child: GlobalTotalChart(monthlyTotals: state.scopedMonthlyTotals)
),
SizedBox(
height: 500,
child: MonthlyCategoriesTotalChart(categoriesMonthlyTotals: state.scopedCategoriesMonthlyTotals)
),
SizedBox(
height: 450,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CategoriesTotalsChart(categoriesTotals: state.scopedCategoriesPositiveTotals, categoriesTotalsPercents: state.scopedCategoriesPositiveTotalsPercents,),
CategoriesTotalsChart(categoriesTotals: state.scopedCategoriesNegativeTotals, categoriesTotalsPercents: state.scopedSimplifiedCategoriesNegativeTotalsPercents,),
],
)
),
],
)
),
);
}
}

View File

@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class AccountCounter extends StatelessWidget {
final Map<String, double> accountsTotals;
const AccountCounter({super.key, required this.accountsTotals});
List<Row> _renderAccountTotals() {
return accountsTotals.entries.toList().map((entry) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("${entry.key}:"),
Text(
NumberFormat('#######.00 €', 'fr_FR').format(entry.value),
style: TextStyle(
fontFamily: 'NovaMono',
fontSize: 15,
color: entry.value > 0 ? const Color.fromARGB(255, 0, 255, 8) : Colors.red
)),
],
)).toList();
}
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(10),
margin: EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: Colors.blue
),
alignment: Alignment.centerRight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: _renderAccountTotals(),
),
);
}
}

View File

@@ -0,0 +1,121 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:tunas/domains/charts/models/chart_item.dart';
class CategoriesTotalsChart extends StatelessWidget {
final List<ChartItem> categoriesTotals;
final List<ChartItem> categoriesTotalsPercents;
const CategoriesTotalsChart({super.key, required this.categoriesTotals, required this.categoriesTotalsPercents});
List<PieChartSectionData> _convertDataForChart() {
var count = 1;
var colors = [
Colors.purple.shade300,
Colors.purple.shade500,
Colors.purple.shade700,
Colors.purple.shade900,
Colors.blue.shade300,
Colors.blue.shade500,
Colors.blue.shade700,
Colors.blue.shade900,
Colors.green.shade300,
Colors.green.shade500,
Colors.green.shade700,
Colors.green.shade900,
Colors.yellow.shade300,
Colors.yellow.shade500,
Colors.yellow.shade700,
Colors.yellow.shade900,
Colors.red.shade300,
Colors.red.shade500,
Colors.red.shade700,
Colors.red.shade900,
];
return categoriesTotalsPercents
.map((item) =>
PieChartSectionData(
value: item.value,
title: item.label,
titleStyle: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w300
),
titlePositionPercentageOffset: 0.8,
borderSide: const BorderSide(width: 0),
radius: 150,
color: colors[count++]
))
.toList();
}
List<Row> _computeLegend() {
return categoriesTotals
.map((item) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("${item.label}: "),
Text(
NumberFormat("#00 €").format(item.value),
style: const TextStyle(
fontFamily: 'NovaMono',
)
)
],
)).toList();
}
@override
Widget build(BuildContext context) {
return Container(
height: 320,
width: 550,
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: Colors.blue
),
child: Expanded(
child: AspectRatio(
aspectRatio: 1.3,
child: Row(
children: [
Expanded(
child: AspectRatio(
aspectRatio: 1,
child: PieChart(
PieChartData(
sections: _convertDataForChart(),
borderData: FlBorderData(
show: false
),
centerSpaceRadius: 0,
sectionsSpace: 2
)
),
)
),
Container(
height: 300,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: Colors.blueGrey
),
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: _computeLegend(),
),
)
),
],
),
)
)
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class MainCounter extends StatelessWidget {
final double value;
const MainCounter({super.key, required this.value});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: Colors.blue
),
alignment: Alignment.centerRight,
child: Text(
NumberFormat('000.00 €', 'fr_FR').format(value),
style: TextStyle(
fontFamily: 'NovaMono',
fontSize: 60,
fontWeight: FontWeight.w500,
color: value > 0 ? const Color.fromARGB(255, 0, 255, 8) : Colors.red
),
),
);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
class MonthlyCategoriesTotalChart extends StatelessWidget {
final Map<String, Map<String, double>> categoriesMonthlyTotals;
const MonthlyCategoriesTotalChart({super.key, required this.categoriesMonthlyTotals});
@override
Widget build(BuildContext context) {
return BarChart(
BarChartData()
);
}
}

View File

@@ -0,0 +1,92 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class GlobalTotalChart extends StatelessWidget {
final List<FlSpot> monthlyTotals;
const GlobalTotalChart({super.key, required this.monthlyTotals});
@override
Widget build(BuildContext context) {
var maxY = 1.0;
var minY = -1.0;
if (monthlyTotals.isNotEmpty) {
maxY = monthlyTotals.map((e) => e.y).toList().reduce((value, element) => value > element ? value : element) + 1000;
minY = monthlyTotals.map((e) => e.y).toList().reduce((value, element) => value < element ? value : element) - 1000;
}
return Padding(
padding: const EdgeInsets.only(
left: 12,
bottom: 12,
right: 20,
top: 20,
),
child: AspectRatio(
aspectRatio: 1,
child: LayoutBuilder(
builder: (context, constraints) => LineChart(
LineChartData(
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
maxContentWidth: 100,
tooltipBgColor: Colors.black,
getTooltipItems: (touchedSpots) {
return touchedSpots.map((LineBarSpot touchedSpot) {
final textStyle = TextStyle(
color: touchedSpot.bar.gradient?.colors[0] ?? touchedSpot.bar.color,
fontWeight: FontWeight.bold,
fontSize: 14,
);
final date = DateTime(DateTime.now().year).add(Duration(days: touchedSpot.x.toInt() - 1));
return LineTooltipItem(
"${NumberFormat('#######.00 €', 'fr_FR').format(touchedSpot.y )} ${date.day}/${date.month}",
textStyle,
);
}).toList();
},
),
handleBuiltInTouches: true,
getTouchLineStart: (data, index) => 0,
),
lineBarsData: [
LineChartBarData(
color: Colors.pink,
spots: monthlyTotals,
isCurved: true,
isStrokeCapRound: true,
barWidth: 3,
belowBarData: BarAreaData(
show: false,
),
dotData: const FlDotData(show: false),
),
],
minY: minY,
maxY: maxY,
titlesData: const FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
gridData: const FlGridData(
show: false,
),
borderData: FlBorderData(show: false),
),
)
)
)
);
}
}

View File

@@ -0,0 +1,41 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class ProfitIndicator extends StatelessWidget {
final List<FlSpot> monthlyTotals;
const ProfitIndicator({super.key, required this.monthlyTotals});
double _profit() {
if (monthlyTotals.isEmpty) {
return 0;
}
final maxDateSpot = monthlyTotals.reduce((value, element) => value.x > element.x ? value : element);
final minDateSpot = monthlyTotals.reduce((value, element) => value.x < element.x ? value : element);
return maxDateSpot.y - minDateSpot.y;
}
@override
Widget build(BuildContext context) {
final profit = _profit();
return Container(
margin: const EdgeInsets.fromLTRB(0, 0, 20, 0),
padding: const EdgeInsets.fromLTRB(10, 5, 10, 5),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(5)
),
child: Text(
"Profit ${NumberFormat('#####00.00 €', 'fr_FR').format(profit)}",
style: TextStyle(
fontFamily: 'NovaMono',
fontSize: 20,
fontWeight: FontWeight.w500,
color: profit > 0 ? const Color.fromARGB(255, 0, 255, 8) : Colors.red
),
)
);
}
}

View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:tunas/domains/charts/chart_bloc.dart';
class YearSelector extends StatelessWidget {
const YearSelector({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<ChartBloc, ChartState>(
builder: (context, state) => Container(
margin: const EdgeInsets.fromLTRB(20, 0, 0, 0),
padding: const EdgeInsets.fromLTRB(5, 0, 5, 0),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(5)
),
child: Row(
children: [
IconButton(
onPressed: () => context.read<ChartBloc>().add(ChartPreviousYear()),
icon: const Icon(Icons.skip_previous)
),
Text(state.currentYear.toString()),
IconButton(
onPressed: () => context.read<ChartBloc>().add(ChartNextYear()),
icon: const Icon(Icons.skip_next)
),
],
)
)
);
}
}