Skip to content

Commit

Permalink
add piggybank detail view
Browse files Browse the repository at this point in the history
  • Loading branch information
dreautall committed Mar 11, 2023
1 parent 7d32ff4 commit 6da5cf4
Show file tree
Hide file tree
Showing 3 changed files with 229 additions and 24 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,22 @@ The app design is heavily influenced by [Bluecoins](https://play.google.com/stor

The app is still pretty much work in progress, but basic features already do work:

- General
- Dark Mode switch according to system settings
- Dashboard
- Multiple charts for the current balance & recent history
- Budget overview for last 30 days.
- Budget overview for last 30 days
- Transactions
- List transactions by date, including (basic) filters.
- List transactions by date, including (basic) filters
- Add & edit transactions with autocomplete, including attachments, split transactions & multi currency support
- Balance Sheet
- List invididual balances

### Planned Features
- Piggy Banks
- Dark Mode
- Detailed Accounts page
- Settings page
- Notification Listener
- More filter options
- ... and many more.

## Screenshots
Expand Down
243 changes: 223 additions & 20 deletions lib/pages/home_piggybank.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import 'dart:ui';

import 'package:chopper/chopper.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

import 'package:waterflyiii/auth.dart';
import 'package:waterflyiii/swagger_fireflyiii_api/firefly_iii.swagger.dart';

import 'package:community_charts_flutter/community_charts_flutter.dart'
as charts;

class HomePiggybank extends StatefulWidget {
const HomePiggybank({
super.key,
Expand Down Expand Up @@ -58,12 +63,16 @@ class _HomePiggybankState extends State<HomePiggybank>
children: <Widget>[
...snapshot.data!.data.map(
(PiggyBankRead piggy) {
final double balance =
double.parse(piggy.attributes.currentAmount ?? "");
final String balanceString = balance.abs().toStringAsFixed(
piggy.attributes.currencyDecimalPlaces ?? 2);
final double currentAmount =
double.tryParse(piggy.attributes.currentAmount ?? "") ??
0;
final String currentString = currentAmount
.abs()
.toStringAsFixed(
piggy.attributes.currencyDecimalPlaces ?? 2);
final double targetAmount =
double.parse(piggy.attributes.targetAmount ?? "");
double.tryParse(piggy.attributes.targetAmount ?? "") ??
0;
final String targetString = targetAmount
.abs()
.toStringAsFixed(
Expand Down Expand Up @@ -92,12 +101,12 @@ class _HomePiggybankState extends State<HomePiggybank>
children: <InlineSpan>[
TextSpan(
text:
"${(balance < 0) ? '-' : ''}${piggy.attributes.currencySymbol}$balanceString",
"${(currentAmount < 0) ? '-' : ''}${piggy.attributes.currencySymbol}$currentString",
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(
color: (balance < 0)
color: (currentAmount < 0)
? Colors.red
: Colors.green,
fontWeight: FontWeight.bold,
Expand All @@ -112,24 +121,17 @@ class _HomePiggybankState extends State<HomePiggybank>
targetAmount != 0
? TextSpan(
text:
"${(piggy.attributes.percentage ?? 0).round()}% of ${piggy.attributes.currencySymbol}$targetAmount")
"${(piggy.attributes.percentage ?? 0).round()}% of ${piggy.attributes.currencySymbol}$targetString")
: const TextSpan(),
],
),
),
onTap: () {
// Piggybank Chart Dialog!
/*showDialog(
context: context,
builder: (BuildContext context) => Dialog.fullscreen(
child: Scaffold(
appBar: AppBar(
title: Text(piggy.attributes.name),
),
body: :TODO:,
),
),
);*/
showDialog(
context: context,
builder: (BuildContext context) =>
PiggyDetails(piggy: piggy),
);
},
),
piggy.attributes.percentage != null
Expand Down Expand Up @@ -158,3 +160,204 @@ class _HomePiggybankState extends State<HomePiggybank>
);
}
}

class PiggyDetails extends StatefulWidget {
const PiggyDetails({
super.key,
required this.piggy,
});

final PiggyBankRead piggy;

@override
State<PiggyDetails> createState() => _PiggyDetailsState();
}

class TimeSeriesChart {
final DateTime time;
final double value;

TimeSeriesChart(this.time, this.value);
}

class _PiggyDetailsState extends State<PiggyDetails> {
Future<List<PiggyBankEventRead>> _fetchChart() async {
final api = FireflyProvider.of(context).api;
if (api == null) {
throw Exception("API unavailable");
}

final resp = await api.apiV1PiggyBanksIdEventsGet(id: widget.piggy.id);
if (!resp.isSuccessful || resp.body == null || resp.body!.data.isEmpty) {
throw Exception("Invalid Response from API");
}

return resp.body!.data.sortedBy<DateTime>((PiggyBankEventRead e) =>
e.attributes.createdAt ?? e.attributes.updatedAt ?? DateTime.now());
}

@override
Widget build(BuildContext context) {
final double currentAmount =
double.tryParse(widget.piggy.attributes.currentAmount ?? "") ?? 0;
final String currentString = currentAmount
.abs()
.toStringAsFixed(widget.piggy.attributes.currencyDecimalPlaces ?? 2);
final double targetAmount =
double.tryParse(widget.piggy.attributes.targetAmount ?? "") ?? 0;
final String targetString = targetAmount
.abs()
.toStringAsFixed(widget.piggy.attributes.currencyDecimalPlaces ?? 2);
final double leftAmount =
double.tryParse(widget.piggy.attributes.leftToSave ?? "") ?? 0;
final String leftString = leftAmount
.abs()
.toStringAsFixed(widget.piggy.attributes.currencyDecimalPlaces ?? 2);
final DateTime? startDate = widget.piggy.attributes.startDate?.toLocal();
final DateTime? targetDate = widget.piggy.attributes.targetDate?.toLocal();

String infoText = "";

if (targetAmount != 0) {
infoText +=
"Target amount: ${widget.piggy.attributes.currencySymbol}$targetString\n";
}
infoText +=
"Saved so far: ${widget.piggy.attributes.currencySymbol}$currentString\n";
if (leftAmount != 0) {
infoText +=
"Left to save: ${widget.piggy.attributes.currencySymbol}$leftString\n";
}
if (startDate != null) {
infoText +=
"Start date: ${DateFormat(DateFormat.YEAR_MONTH_DAY).format(startDate)}\n";
}
if (targetDate != null) {
infoText +=
"Target date: ${DateFormat(DateFormat.YEAR_MONTH_DAY).format(targetDate)}\n";
}

return SimpleDialog(
title: Text(widget.piggy.attributes.name),
clipBehavior: Clip.hardEdge,
children: <Widget>[
widget.piggy.attributes.percentage != null
? LinearProgressIndicator(
value: widget.piggy.attributes.percentage! / 100,
)
: const Divider(height: 0),
Padding(
padding: const EdgeInsets.fromLTRB(24, 12, 24, 0),
child: Text(infoText.strip()),
),
FutureBuilder(
future: _fetchChart(),
builder: (BuildContext context,
AsyncSnapshot<List<PiggyBankEventRead>> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData) {
final List<charts.Series<TimeSeriesChart, DateTime>> chartData =
[];
final List<charts.TickSpec<DateTime>> ticks = [];
final List<TimeSeriesChart> data = [];

double total = 0;

if (widget.piggy.attributes.startDate != null) {
data.add(TimeSeriesChart(
widget.piggy.attributes.startDate!,
0,
));
ticks.add(charts.TickSpec(
widget.piggy.attributes.startDate!.toLocal()));
}

for (PiggyBankEventRead e in snapshot.data!) {
final DateTime? date =
e.attributes.createdAt ?? e.attributes.updatedAt;
final amount =
double.tryParse(e.attributes.amount ?? "") ?? 0;
if (date == null || amount == 0) {
continue;
}
total += amount;
data.add(TimeSeriesChart(date, total));
ticks.add(charts.TickSpec(date.toLocal()));
}
chartData.add(
charts.Series<TimeSeriesChart, DateTime>(
id: widget.piggy.id,
domainFn: (TimeSeriesChart d, _) => d.time.toLocal(),
measureFn: (TimeSeriesChart d, _) => d.value,
data: data,
),
);

return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: SizedBox(
height: 300,
child: charts.TimeSeriesChart(
chartData,
animate: true,
primaryMeasureAxis: charts.NumericAxisSpec(
tickProviderSpec:
const charts.BasicNumericTickProviderSpec(
//desiredTickCount: 6,
desiredMaxTickCount: 6,
desiredMinTickCount: 4,
zeroBound: true,
),
renderSpec: charts.SmallTickRendererSpec(
labelStyle: charts.TextStyleSpec(
color: charts.ColorUtil.fromDartColor(
Theme.of(context).colorScheme.onSurfaceVariant),
),
),
),
domainAxis: charts.DateTimeAxisSpec(
tickFormatterSpec: charts.BasicDateTimeTickFormatterSpec
.fromDateFormat(
DateFormat(DateFormat.ABBR_MONTH_DAY)),
tickProviderSpec:
const charts.AutoDateTimeTickProviderSpec(
includeTime: false),
renderSpec: charts.SmallTickRendererSpec(
labelStyle: charts.TextStyleSpec(
color: charts.ColorUtil.fromDartColor(
Theme.of(context).colorScheme.onSurfaceVariant),
),
),
),
),
),
);
} else if (snapshot.hasError) {
print("has error ${snapshot.error}, popping view");
Navigator.of(context).pop();
return const SizedBox();
} else {
return const Padding(
padding: EdgeInsets.all(8),
child: Center(
child: CircularProgressIndicator(),
),
);
}
}),
OverflowBar(
alignment: MainAxisAlignment.end,
children: <Widget>[
TextButton(
onPressed: () async {
Navigator.of(context).pop();
},
child: const Text("Close"),
),
const SizedBox(width: 12),
],
),
],
);
}
}
1 change: 1 addition & 0 deletions lib/pages/home_transactions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class _HomeTransactionsState extends State<HomeTransactions>
widget.key!,
<Widget>[
IconButton(
// :TODO: turn blue when filter is set.. if feasible
icon: const Icon(Icons.tune),
tooltip: 'Filter List',
onPressed: () async {
Expand Down

0 comments on commit 6da5cf4

Please sign in to comment.