Compare commits
8 commits
e68bd611bc
...
9fac138386
Author | SHA1 | Date | |
---|---|---|---|
9fac138386 | |||
|
2d50509163 | ||
ef52caa836 | |||
300f359070 | |||
6530b58c19 | |||
2339739ff8 | |||
5f99e35709 | |||
fd04bc1dc4 |
27 changed files with 1242 additions and 774 deletions
|
@ -1,6 +1,8 @@
|
|||
# newVersion
|
||||
# 2.0.0
|
||||
- Upgrade dependencies
|
||||
- Use less `await`s in WalletManager class
|
||||
- Added debt management
|
||||
- Changed android app ID
|
||||
|
||||
# 1.1.1
|
||||
- Removed deprecated code
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
# prasule
|
||||
[![Codemagic build status](https://api.codemagic.io/apps/64faee78aae8c48abc70dbc6/64faee78aae8c48abc70dbc5/status_badge.svg)](https://codemagic.io/apps/64faee78aae8c48abc70dbc6/64faee78aae8c48abc70dbc5/latest_build) [![Bug issue count](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.mnau.xyz%2Fapi%2Fv1%2Frepos%2Fhernik%2Fprasule%2Fissues%3Flabels%3DKind%2FBug&query=%24.length&logo=forgejo&label=bug%20issues&color=red)](https://git.mnau.xyz/hernik/prasule/issues?q=&type=all&sort=&state=open&labels=144&milestone=0&project=0&assignee=0&poster=0) [![wakatime](https://wakatime.com/badge/user/17178fab-a33c-430f-a764-7b3f26c7b966/project/bf1f40b0-c8c0-4f72-8ad6-c861ecdcc90c.svg)](https://wakatime.com/badge/user/17178fab-a33c-430f-a764-7b3f26c7b966/project/bf1f40b0-c8c0-4f72-8ad6-c861ecdcc90c) [![Translation status](https://hosted.weblate.org/widget/prasule/svg-badge.svg)](https://hosted.weblate.org/engage/prasule/) [![Please don't upload to GitHub](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page)
|
||||
[![Coverage](https://sq.mnau.xyz/api/project_badges/measure?project=prasule&metric=coverage&token=sqb_098860bff1a308465ca633fe569ec3d24c9d8e4b)](https://sq.mnau.xyz/dashboard?id=prasule) [![Maintainability Rating](https://sq.mnau.xyz/api/project_badges/measure?project=prasule&metric=sqale_rating&token=sqb_098860bff1a308465ca633fe569ec3d24c9d8e4b)](https://sq.mnau.xyz/dashboard?id=prasule) [![Bug issue count](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.mnau.xyz%2Fapi%2Fv1%2Frepos%2Fhernik%2Fprasule%2Fissues%3Flabels%3DKind%2FBug&query=%24.length&logo=forgejo&label=bug%20issues&color=red)](https://git.mnau.xyz/hernik/prasule/issues?q=&type=all&sort=&state=open&labels=144&milestone=0&project=0&assignee=0&poster=0) [![Time spent on project](https://wt.mnau.xyz/api/badge/hernik/interval:today/project:prasule)](https://wakatime.com/badge/user/17178fab-a33c-430f-a764-7b3f26c7b966/project/bf1f40b0-c8c0-4f72-8ad6-c861ecdcc90c) [![Translation status](https://hosted.weblate.org/widget/prasule/svg-badge.svg)](https://hosted.weblate.org/engage/prasule/) [![Please don't upload to GitHub](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page) [![Codemagic build status](https://api.codemagic.io/apps/64faee78aae8c48abc70dbc6/6698dab78938fa88eaef9f82/status_badge.svg)](https://codemagic.io/app/64faee78aae8c48abc70dbc6/6698dab78938fa88eaef9f82/latest_build)
|
||||
|
||||
Expense manager
|
||||
|
||||
- [Apple Testflight](https://testflight.apple.com/join/C22pcnPc)
|
||||
- [Google Play beta testing](https://play.google.com/store/apps/details?id=cafe.caras.prasule)
|
||||
|
||||
## License
|
||||
``` Prašule - simple, private & open-source expense tracker
|
||||
|
|
|
@ -48,7 +48,7 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "cafe.caras.prasule"
|
||||
applicationId "wtf.caras.prasule"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||
minSdkVersion 21
|
||||
|
|
|
@ -37,6 +37,8 @@ class DebtEntry {
|
|||
String name;
|
||||
|
||||
/// List of people who payed
|
||||
@JsonKey(defaultValue: DebtPerson.unknownPerson)
|
||||
List<DebtPerson> whoPayed;
|
||||
@JsonKey(defaultValue: _defaultDebtPayers)
|
||||
final List<DebtPerson> whoPayed;
|
||||
}
|
||||
|
||||
List<DebtPerson> _defaultDebtPayers() => [DebtPerson.unknownPerson()];
|
||||
|
|
|
@ -19,7 +19,7 @@ DebtEntry _$DebtEntryFromJson(Map<String, dynamic> json) {
|
|||
whoPayed: (json['whoPayed'] as List<dynamic>?)
|
||||
?.map((e) => DebtPerson.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
DebtPerson.unknownPerson(),
|
||||
_defaultDebtPayers(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ part 'debt_person.g.dart';
|
|||
/// Represents a single person in a debt scenario
|
||||
class DebtPerson {
|
||||
/// Represents a single person in a debt scenario
|
||||
DebtPerson({required this.id, required this.name});
|
||||
DebtPerson({required this.id, required this.name, this.bankAccount});
|
||||
|
||||
/// Default [DebtPerson] instance for json_serializable
|
||||
factory DebtPerson.unknownPerson() => DebtPerson(id: -1, name: "Unknown");
|
||||
|
@ -25,4 +25,9 @@ class DebtPerson {
|
|||
/// Identifier that the user will see
|
||||
@JsonKey(defaultValue: "Unknown")
|
||||
String name;
|
||||
|
||||
/// Person's bank account
|
||||
///
|
||||
/// Used to generate a QR code payment
|
||||
String? bankAccount;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ DebtPerson _$DebtPersonFromJson(Map<String, dynamic> json) {
|
|||
return DebtPerson(
|
||||
id: (json['id'] as num).toInt(),
|
||||
name: json['name'] as String? ?? 'Unknown',
|
||||
bankAccount: json['bankAccount'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -22,4 +23,5 @@ Map<String, dynamic> _$DebtPersonToJson(DebtPerson instance) =>
|
|||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'bankAccount': instance.bankAccount,
|
||||
};
|
||||
|
|
|
@ -11,8 +11,8 @@ class DebtScenario {
|
|||
required this.id,
|
||||
required this.name,
|
||||
required this.isArchived,
|
||||
required this.people,
|
||||
this.entries = const [],
|
||||
this.people = const [],
|
||||
});
|
||||
|
||||
/// Generates a class instance from a Map
|
||||
|
@ -22,7 +22,7 @@ class DebtScenario {
|
|||
/// Converts the data in this instance into a Map
|
||||
Map<String, dynamic> toJson() => _$DebtScenarioToJson(this);
|
||||
|
||||
/// Unique identified
|
||||
/// Unique identifier
|
||||
@JsonKey(disallowNullValue: true)
|
||||
final int id;
|
||||
|
||||
|
@ -36,11 +36,29 @@ class DebtScenario {
|
|||
|
||||
/// All entries
|
||||
@JsonKey(defaultValue: [])
|
||||
List<DebtEntry> entries;
|
||||
final List<DebtEntry> entries;
|
||||
|
||||
/// All people
|
||||
@JsonKey(defaultValue: _defaultPeopleList)
|
||||
List<DebtPerson> people;
|
||||
final List<DebtPerson> people;
|
||||
|
||||
/// Getter for the next unused unique number ID for a [DebtPerson]
|
||||
int get nextPersonId {
|
||||
var id = 1;
|
||||
while (people.where((element) => element.id == id).isNotEmpty) {
|
||||
id++; // create unique ID
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
/// Getter for the next unused unique number ID for a [DebtEntry]
|
||||
int get nextEntryId {
|
||||
var id = 1;
|
||||
while (entries.where((element) => element.id == id).isNotEmpty) {
|
||||
id++; // create unique ID
|
||||
}
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
List<DebtPerson> _defaultPeopleList() => [DebtPerson.unknownPerson()];
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:intl/intl.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:prasule/api/category.dart';
|
||||
import 'package:prasule/api/debt_scenario.dart';
|
||||
import 'package:prasule/api/entry_data.dart';
|
||||
import 'package:prasule/api/recurring_entry.dart';
|
||||
import 'package:prasule/api/wallet_entry.dart';
|
||||
|
@ -31,6 +32,7 @@ class Wallet {
|
|||
this.categories = const [],
|
||||
this.entries = const [],
|
||||
this.recurringEntries = const [],
|
||||
this.debts = const [],
|
||||
this.starterBalance = 0,
|
||||
});
|
||||
|
||||
|
@ -53,6 +55,10 @@ class Wallet {
|
|||
@JsonKey(defaultValue: [])
|
||||
final List<WalletSingleEntry> entries;
|
||||
|
||||
/// List of user's [DebtScenario]s
|
||||
@JsonKey(defaultValue: [])
|
||||
final List<DebtScenario> debts;
|
||||
|
||||
/// The starting balance of the wallet
|
||||
///
|
||||
/// Used to calculate current balance
|
||||
|
@ -88,6 +94,16 @@ class Wallet {
|
|||
return id;
|
||||
}
|
||||
|
||||
/// Getter for the next unused unique number ID in the wallet's **debts**
|
||||
/// list
|
||||
int get nextDebtId {
|
||||
var id = 0;
|
||||
while (debts.where((element) => element.id == id).isNotEmpty) {
|
||||
id++; // create unique ID
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
/// Handles adding recurring entries to the entry list
|
||||
void recurEntries() {
|
||||
final n = DateTime.now();
|
||||
|
|
|
@ -25,6 +25,10 @@ Wallet _$WalletFromJson(Map<String, dynamic> json) => Wallet(
|
|||
RecurringWalletEntry.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
debts: (json['debts'] as List<dynamic>?)
|
||||
?.map((e) => DebtScenario.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
starterBalance: (json['starterBalance'] as num?)?.toDouble() ?? 0,
|
||||
);
|
||||
|
||||
|
@ -33,6 +37,7 @@ Map<String, dynamic> _$WalletToJson(Wallet instance) => <String, dynamic>{
|
|||
'name': instance.name,
|
||||
'categories': instance.categories,
|
||||
'entries': instance.entries,
|
||||
'debts': instance.debts,
|
||||
'starterBalance': instance.starterBalance,
|
||||
'currency': instance.currency,
|
||||
};
|
||||
|
|
|
@ -16,7 +16,7 @@ import 'package:prasule/main.dart';
|
|||
/// Used for [Wallet]-managing operations
|
||||
class WalletManager {
|
||||
/// Currently selected wallet
|
||||
static Wallet? selectedWallet;
|
||||
static late Wallet selectedWallet;
|
||||
|
||||
/// Path to the directory with wallet files
|
||||
///
|
||||
|
|
|
@ -122,5 +122,14 @@
|
|||
"incomePerYearCategory":"Příjmy podle kategorie za rok {year}",
|
||||
"incomePerMonthCategory":"Příjmy podle kategorie za měsíc {monthYear}",
|
||||
"selectYear":"Zvolte rok",
|
||||
"selectMonth":"Zvolte měsíc a rok"
|
||||
"selectMonth":"Zvolte měsíc a rok",
|
||||
"debts":"Dlužníček",
|
||||
"debtNamePlaceholder":"Dluhy přátel",
|
||||
"people":"Lidé",
|
||||
"addSomePeople":"Přidej lidi pomocí tlačítka níže",
|
||||
"bankAccount":"Číslo bankovního účtu",
|
||||
"noDebtScenarios":"Žádné seznamy dlužníků :(",
|
||||
"noDebtScenariosSub":"Nový můžete vytvořit pomocí plovoucího tlačítka.",
|
||||
"noPersonError":"Musíte vložit alespoň jednoho člověka!",
|
||||
"unnamed":"Bez jména"
|
||||
}
|
|
@ -313,5 +313,14 @@
|
|||
}
|
||||
},
|
||||
"selectYear":"Select a year",
|
||||
"selectMonth":"Select a month and year"
|
||||
"selectMonth":"Select a month and year",
|
||||
"debts":"Debts",
|
||||
"debtNamePlaceholder":"Friends' debts",
|
||||
"people":"People",
|
||||
"addSomePeople":"Add people using the button below",
|
||||
"bankAccount":"Bank account number",
|
||||
"noDebtScenarios":"No debt scenarios :(",
|
||||
"noDebtScenariosSub":"Create one using the floating action button.",
|
||||
"noPersonError":"You need to add at least one person!",
|
||||
"unnamed":"Unnamed"
|
||||
}
|
1
lib/l10n/app_sk.arb
Normal file
1
lib/l10n/app_sk.arb
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -10,9 +10,9 @@ import 'package:flutter/services.dart';
|
|||
import 'package:prasule/pw/platformwidget.dart';
|
||||
|
||||
/// A [PlatformWidget] implementation of a text field
|
||||
class PlatformField extends PlatformWidget<TextField, CupertinoTextField> {
|
||||
const PlatformField({
|
||||
super.key,
|
||||
class PlatformField extends PlatformWidget<TextFormField, CupertinoTextField> {
|
||||
const PlatformField(
|
||||
{super.key,
|
||||
this.controller,
|
||||
this.enabled,
|
||||
this.labelText,
|
||||
|
@ -29,7 +29,7 @@ class PlatformField extends PlatformWidget<TextField, CupertinoTextField> {
|
|||
this.inputBorder = const OutlineInputBorder(),
|
||||
this.suffix,
|
||||
this.prefix,
|
||||
});
|
||||
this.validator});
|
||||
final TextEditingController? controller;
|
||||
final bool? enabled;
|
||||
final bool obscureText;
|
||||
|
@ -46,9 +46,10 @@ class PlatformField extends PlatformWidget<TextField, CupertinoTextField> {
|
|||
final FocusNode? focusNode;
|
||||
final Widget? suffix;
|
||||
final Widget? prefix;
|
||||
final String? Function(String?)? validator;
|
||||
|
||||
@override
|
||||
TextField createAndroidWidget(BuildContext context) => TextField(
|
||||
TextFormField createAndroidWidget(BuildContext context) => TextFormField(
|
||||
textAlign: textAlign,
|
||||
controller: controller,
|
||||
enabled: enabled,
|
||||
|
@ -67,6 +68,7 @@ class PlatformField extends PlatformWidget<TextField, CupertinoTextField> {
|
|||
onChanged: onChanged,
|
||||
autofillHints: autofillHints,
|
||||
maxLines: maxLines,
|
||||
validator: validator,
|
||||
);
|
||||
|
||||
@override
|
||||
|
|
|
@ -5,12 +5,13 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:prasule/pw/platformroute.dart';
|
||||
import 'package:prasule/views/graph_view.dart';
|
||||
import 'package:prasule/views/debts/debt_view.dart';
|
||||
import 'package:prasule/views/graphs/graph_view.dart';
|
||||
import 'package:prasule/views/home.dart';
|
||||
import 'package:prasule/views/recurring_view.dart';
|
||||
import 'package:prasule/views/recurring/recurring_view.dart';
|
||||
|
||||
/// Makes the drawer because I won't enter the same code in every view
|
||||
Drawer makeDrawer(BuildContext context, int page) => Drawer(
|
||||
Drawer makeDrawer(BuildContext context, Pages page) => Drawer(
|
||||
child: ListView(
|
||||
children: [
|
||||
const DrawerHeader(child: Text("Prašule")),
|
||||
|
@ -19,9 +20,9 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer(
|
|||
title: Text(
|
||||
AppLocalizations.of(context).home,
|
||||
),
|
||||
selected: page == 1,
|
||||
selected: page == Pages.home,
|
||||
onTap: () {
|
||||
if (page == 1) {
|
||||
if (page == Pages.home) {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
|
@ -34,9 +35,9 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer(
|
|||
title: Text(
|
||||
AppLocalizations.of(context).graphs,
|
||||
),
|
||||
selected: page == 2,
|
||||
selected: page == Pages.graphs,
|
||||
onTap: () {
|
||||
if (page == 2) {
|
||||
if (page == Pages.graphs) {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
|
@ -49,9 +50,9 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer(
|
|||
title: Text(
|
||||
AppLocalizations.of(context).recurringPayments,
|
||||
),
|
||||
selected: page == 3,
|
||||
selected: page == Pages.recurringEntries,
|
||||
onTap: () {
|
||||
if (page == 3) {
|
||||
if (page == Pages.recurringEntries) {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
|
@ -60,6 +61,37 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer(
|
|||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.people),
|
||||
title: Text(
|
||||
AppLocalizations.of(context).debts,
|
||||
),
|
||||
selected: page == Pages.debts,
|
||||
onTap: () {
|
||||
if (page == Pages.debts) {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pushReplacement(
|
||||
platformRoute((p0) => const DebtView()),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
/// All the pages that drawer can navigate to
|
||||
enum Pages {
|
||||
/// [HomeView]
|
||||
home,
|
||||
|
||||
/// [GraphView]
|
||||
graphs,
|
||||
|
||||
/// [RecurringEntriesView]
|
||||
recurringEntries,
|
||||
|
||||
/// [DebtView]
|
||||
debts
|
||||
}
|
||||
|
|
141
lib/views/debts/debt_view.dart
Normal file
141
lib/views/debts/debt_view.dart
Normal file
|
@ -0,0 +1,141 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:prasule/api/debt_scenario.dart';
|
||||
import 'package:prasule/api/wallet.dart';
|
||||
import 'package:prasule/api/wallet_manager.dart';
|
||||
import 'package:prasule/main.dart';
|
||||
import 'package:prasule/pw/platformroute.dart';
|
||||
import 'package:prasule/util/drawer.dart';
|
||||
import 'package:prasule/views/debts/setup_debt_scenario.dart';
|
||||
import 'package:prasule/views/setup.dart';
|
||||
|
||||
/// Shows the selected [DebtScenario]
|
||||
class DebtView extends StatefulWidget {
|
||||
/// Shows the selected [DebtScenario]
|
||||
const DebtView({super.key});
|
||||
|
||||
@override
|
||||
State<DebtView> createState() => _DebtViewState();
|
||||
}
|
||||
|
||||
class _DebtViewState extends State<DebtView> {
|
||||
List<Wallet> wallets = [];
|
||||
void loadWallet() {
|
||||
wallets = WalletManager.listWallets();
|
||||
if (wallets.isEmpty && mounted) {
|
||||
unawaited(
|
||||
Navigator.of(context)
|
||||
.pushReplacement(platformRoute((c) => const SetupView())),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
loadWallet();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: WalletManager.selectedWallet.debts.isEmpty
|
||||
? 1
|
||||
: WalletManager.selectedWallet.debts.length,
|
||||
child: Scaffold(
|
||||
floatingActionButton: FloatingActionButton(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () {
|
||||
Navigator.of(context)
|
||||
.push(platformRoute((c) => const SetupDebtScenario()))
|
||||
.then((v) {
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
appBar: AppBar(
|
||||
title: DropdownButton<int>(
|
||||
value: wallets.indexOf(
|
||||
wallets
|
||||
.where((w) => w.name == WalletManager.selectedWallet.name)
|
||||
.first,
|
||||
),
|
||||
items: [
|
||||
...wallets.map(
|
||||
(e) => DropdownMenuItem(
|
||||
value: wallets.indexOf(
|
||||
e,
|
||||
),
|
||||
child: Text(e.name),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: -1,
|
||||
child: Text(AppLocalizations.of(context).newWallet),
|
||||
),
|
||||
],
|
||||
onChanged: (v) async {
|
||||
if (v == null || v == -1) {
|
||||
await Navigator.of(context).push(
|
||||
platformRoute(
|
||||
(c) => const SetupView(
|
||||
newWallet: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
wallets = WalletManager.listWallets();
|
||||
logger.i(wallets.length);
|
||||
WalletManager.selectedWallet = wallets.last;
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
WalletManager.selectedWallet = wallets[v];
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
bottom: TabBar(
|
||||
tabs: (WalletManager.selectedWallet.debts.isEmpty)
|
||||
? [Text(AppLocalizations.of(context).welcome)]
|
||||
: List.generate(
|
||||
WalletManager.selectedWallet.debts.length,
|
||||
(index) => Tab(
|
||||
text: WalletManager.selectedWallet.debts[index].name,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
drawer: makeDrawer(context, Pages.debts),
|
||||
body: TabBarView(
|
||||
children: (WalletManager.selectedWallet.debts.isEmpty)
|
||||
? [
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Text(
|
||||
AppLocalizations.of(context).noDebtScenarios,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
Text(AppLocalizations.of(context).noDebtScenariosSub),
|
||||
],
|
||||
),
|
||||
]
|
||||
: List.generate(
|
||||
WalletManager.selectedWallet.debts.length,
|
||||
(c) => const Placeholder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
254
lib/views/debts/setup_debt_scenario.dart
Normal file
254
lib/views/debts/setup_debt_scenario.dart
Normal file
|
@ -0,0 +1,254 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:prasule/api/debt_person.dart';
|
||||
import 'package:prasule/api/debt_scenario.dart';
|
||||
import 'package:prasule/api/wallet_manager.dart';
|
||||
import 'package:prasule/pw/platformfield.dart';
|
||||
import 'package:prasule/util/show_message.dart';
|
||||
|
||||
/// Used to create and/or edit [DebtScenario]s
|
||||
class SetupDebtScenario extends StatefulWidget {
|
||||
/// Used to create and/or edit [DebtScenario]s
|
||||
const SetupDebtScenario({super.key, this.toEdit});
|
||||
|
||||
/// If not null, loads this [DebtScenario]'s data for editing
|
||||
final DebtScenario? toEdit;
|
||||
|
||||
@override
|
||||
State<SetupDebtScenario> createState() => _SetupDebtScenarioState();
|
||||
}
|
||||
|
||||
class _SetupDebtScenarioState extends State<SetupDebtScenario> {
|
||||
late DebtScenario _scenario;
|
||||
final _nameController = TextEditingController();
|
||||
final GlobalKey<FormState> _formKey = GlobalKey();
|
||||
|
||||
/// Stores data for each [ExpansionPanel]
|
||||
final List<bool> _isOpen = [];
|
||||
final List<TextEditingController> _panelControllers = [];
|
||||
bool _isLoading = true;
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (!_isLoading) return;
|
||||
_scenario = widget.toEdit ??
|
||||
DebtScenario(
|
||||
id: WalletManager.selectedWallet.nextDebtId,
|
||||
name: AppLocalizations.of(context).debtNamePlaceholder,
|
||||
isArchived: false,
|
||||
people: [],
|
||||
entries: [],
|
||||
);
|
||||
|
||||
// Load stuff from the scenario
|
||||
_nameController.text = _scenario.name;
|
||||
_isOpen.addAll(List.filled(_scenario.people.length, false));
|
||||
_panelControllers.addAll(
|
||||
List.filled(_scenario.people.length * 2, TextEditingController()),
|
||||
);
|
||||
|
||||
_isLoading = false;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: SingleChildScrollView(
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.95,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
child: PlatformField(
|
||||
labelText: AppLocalizations.of(context).name,
|
||||
controller: _nameController,
|
||||
validator: (input) {
|
||||
if (input == null || input.isEmpty) {
|
||||
return AppLocalizations.of(context).errorEmptyName;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
Text(AppLocalizations.of(context).people),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
LimitedBox(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.6,
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: (_scenario.people.isEmpty)
|
||||
? Text(AppLocalizations.of(context).addSomePeople)
|
||||
: ExpansionPanelList(
|
||||
expansionCallback: (panelIndex, isExpanded) {
|
||||
_isOpen[panelIndex] = isExpanded;
|
||||
setState(() {});
|
||||
},
|
||||
children: List<ExpansionPanel>.generate(
|
||||
_scenario.people.length,
|
||||
(index) => ExpansionPanel(
|
||||
isExpanded: _isOpen[index],
|
||||
headerBuilder: (context, isOpen) => Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context)
|
||||
.size
|
||||
.width *
|
||||
0.45,
|
||||
child: PlatformField(
|
||||
labelText:
|
||||
AppLocalizations.of(context)
|
||||
.name,
|
||||
controller:
|
||||
_panelControllers[index],
|
||||
onChanged: (input) {
|
||||
_scenario.people[index].name =
|
||||
input;
|
||||
setState(() {});
|
||||
},
|
||||
validator: (input) {
|
||||
if (input == null ||
|
||||
input.isEmpty) {
|
||||
return AppLocalizations.of(
|
||||
context,
|
||||
).errorEmptyName;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_scenario.people.removeAt(index);
|
||||
_panelControllers.removeRange(
|
||||
index,
|
||||
index + 2,
|
||||
);
|
||||
_isOpen.removeAt(index);
|
||||
setState(() {});
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.delete_forever,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context)
|
||||
.size
|
||||
.width *
|
||||
0.8,
|
||||
child: PlatformField(
|
||||
labelText:
|
||||
AppLocalizations.of(context)
|
||||
.bankAccount,
|
||||
controller: _panelControllers[
|
||||
index + 1],
|
||||
onChanged: (input) {
|
||||
_scenario.people[index]
|
||||
.bankAccount =
|
||||
(input.isEmpty)
|
||||
? null
|
||||
: input;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_scenario.people.add(
|
||||
DebtPerson(id: _scenario.nextPersonId, name: ""),
|
||||
);
|
||||
_isOpen.add(false);
|
||||
_panelControllers.addAll(
|
||||
[TextEditingController(), TextEditingController()],
|
||||
);
|
||||
setState(() {});
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
if (_scenario.people.isEmpty) {
|
||||
showMessage(
|
||||
AppLocalizations.of(context).noPersonError,
|
||||
context,
|
||||
);
|
||||
return;
|
||||
}
|
||||
_scenario.name = _nameController.text;
|
||||
if (widget.toEdit != null) {
|
||||
// If editing, only replace
|
||||
WalletManager.selectedWallet.debts[WalletManager
|
||||
.selectedWallet.debts
|
||||
.indexWhere((d) => d.id == widget.toEdit!.id)] =
|
||||
_scenario;
|
||||
} else {
|
||||
// else add new
|
||||
WalletManager.selectedWallet.debts.add(_scenario);
|
||||
}
|
||||
WalletManager.saveWallet(WalletManager.selectedWallet);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).save),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,723 +0,0 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:prasule/api/category.dart';
|
||||
import 'package:prasule/api/wallet.dart';
|
||||
import 'package:prasule/api/wallet_manager.dart';
|
||||
import 'package:prasule/main.dart';
|
||||
import 'package:prasule/pw/platformroute.dart';
|
||||
import 'package:prasule/util/drawer.dart';
|
||||
import 'package:prasule/util/graphs.dart';
|
||||
import 'package:prasule/util/utils.dart';
|
||||
import 'package:prasule/views/settings/settings.dart';
|
||||
import 'package:prasule/views/setup.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:wheel_chooser/wheel_chooser.dart';
|
||||
|
||||
/// Shows data from a [Wallet] in graphs
|
||||
class GraphView extends StatefulWidget {
|
||||
/// Shows data from a [Wallet] in graphs
|
||||
const GraphView({super.key});
|
||||
|
||||
@override
|
||||
State<GraphView> createState() => _GraphViewState();
|
||||
}
|
||||
|
||||
class _GraphViewState extends State<GraphView> {
|
||||
var _selectedDate = DateTime.now();
|
||||
Wallet? selectedWallet;
|
||||
List<Wallet> wallets = [];
|
||||
String? locale;
|
||||
bool yearly = true;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
locale ??= Localizations.localeOf(context).languageCode;
|
||||
}
|
||||
|
||||
List<double> generateChartData(EntryType type) {
|
||||
final d = _selectedDate.add(const Duration(days: 31));
|
||||
final data = List<double>.filled(
|
||||
yearly ? 12 : DateTime(d.year, d.month, 0).day,
|
||||
0,
|
||||
);
|
||||
if (selectedWallet == null) return [];
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
final entriesForRange = selectedWallet!.entries.where(
|
||||
(element) =>
|
||||
((!yearly)
|
||||
? element.date.month == _selectedDate.month &&
|
||||
element.date.year == _selectedDate.year &&
|
||||
element.date.day == i + 1
|
||||
: element.date.month == i + 1 &&
|
||||
element.date.year == _selectedDate.year) &&
|
||||
element.type == type,
|
||||
);
|
||||
var sum = 0.0;
|
||||
for (final e in entriesForRange) {
|
||||
sum += e.data.amount;
|
||||
}
|
||||
data[i] = sum;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
final availableYears = <WheelChoice<int>>[];
|
||||
|
||||
void loadWallet() {
|
||||
wallets = WalletManager.listWallets();
|
||||
if (wallets.isEmpty && mounted) {
|
||||
unawaited(
|
||||
Navigator.of(context)
|
||||
.pushReplacement(platformRoute((c) => const SetupView())),
|
||||
);
|
||||
return;
|
||||
}
|
||||
selectedWallet = wallets.first;
|
||||
availableYears.clear();
|
||||
for (final entry in selectedWallet!.entries) {
|
||||
if (!availableYears.any((element) => element.value == entry.date.year)) {
|
||||
availableYears.add(
|
||||
WheelChoice<int>(
|
||||
value: entry.date.year,
|
||||
title: entry.date.year.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
int? chartType;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
loadWallet();
|
||||
SharedPreferences.getInstance().then((s) {
|
||||
chartType = s.getInt("yearlygraph") ?? 1;
|
||||
logger.d(chartType);
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: 2,
|
||||
child: Scaffold(
|
||||
floatingActionButton: Tooltip(
|
||||
message: AppLocalizations.of(context).changeDate,
|
||||
child: FloatingActionButton(
|
||||
child: const Icon(Icons.calendar_month),
|
||||
onPressed: () async {
|
||||
var selectedYear = _selectedDate.year;
|
||||
var selectedMonth = _selectedDate.month;
|
||||
await showAdaptiveDialog<void>(
|
||||
context: context,
|
||||
builder: (c) => AlertDialog.adaptive(
|
||||
title: Text(
|
||||
yearly
|
||||
? AppLocalizations.of(context).selectYear
|
||||
: AppLocalizations.of(context).selectMonth,
|
||||
),
|
||||
content: LimitedBox(
|
||||
maxHeight: MediaQuery.of(context).size.width * 0.7,
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 5,
|
||||
children: [
|
||||
if (!yearly)
|
||||
SizedBox(
|
||||
width: 120,
|
||||
height: 100,
|
||||
child: WheelChooser<int>.choices(
|
||||
onChoiceChanged: (v) {
|
||||
selectedMonth = v as int;
|
||||
},
|
||||
startPosition: _selectedDate.month - 1,
|
||||
choices: List<WheelChoice<int>>.generate(
|
||||
12,
|
||||
(index) => WheelChoice(
|
||||
value: index + 1,
|
||||
title: DateFormat.MMMM(locale ?? "en").format(
|
||||
DateTime(
|
||||
_selectedDate.year,
|
||||
index + 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 100,
|
||||
width: 80,
|
||||
child: WheelChooser<int>.choices(
|
||||
startPosition: availableYears.indexWhere(
|
||||
(element) => element.value == _selectedDate.year,
|
||||
),
|
||||
onChoiceChanged: (v) {
|
||||
selectedYear = v as int;
|
||||
},
|
||||
choices: availableYears,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_selectedDate = DateTime(selectedYear, selectedMonth);
|
||||
Navigator.of(c).pop();
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
appBar: AppBar(
|
||||
bottom: TabBar(
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Text(AppLocalizations.of(context).expenses),
|
||||
),
|
||||
Tab(
|
||||
child: Text(AppLocalizations.of(context).incomePlural),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: DropdownButton<int>(
|
||||
value: (selectedWallet == null)
|
||||
? -1
|
||||
: wallets.indexOf(
|
||||
wallets.where((w) => w.name == selectedWallet!.name).first,
|
||||
),
|
||||
items: [
|
||||
...wallets.map(
|
||||
(e) => DropdownMenuItem(
|
||||
value: wallets.indexOf(
|
||||
e,
|
||||
),
|
||||
child: Text(e.name),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: -1,
|
||||
child: Text(AppLocalizations.of(context).newWallet),
|
||||
),
|
||||
],
|
||||
onChanged: (v) async {
|
||||
if (v == null || v == -1) {
|
||||
await Navigator.of(context).push(
|
||||
platformRoute(
|
||||
(c) => const SetupView(
|
||||
newWallet: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
wallets = WalletManager.listWallets();
|
||||
logger.i(wallets.length);
|
||||
selectedWallet = wallets.last;
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
selectedWallet = wallets[v];
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
AppLocalizations.of(context).settings,
|
||||
AppLocalizations.of(context).about,
|
||||
].map((e) => PopupMenuItem(value: e, child: Text(e))).toList(),
|
||||
onSelected: (value) {
|
||||
if (value == AppLocalizations.of(context).settings) {
|
||||
Navigator.of(context)
|
||||
.push(
|
||||
platformRoute(
|
||||
(context) => const SettingsView(),
|
||||
),
|
||||
)
|
||||
.then((value) async {
|
||||
selectedWallet =
|
||||
WalletManager.loadWallet(selectedWallet!.name);
|
||||
final s = await SharedPreferences.getInstance();
|
||||
chartType = s.getInt("monthlygraph") ?? 2;
|
||||
setState(() {});
|
||||
});
|
||||
} else if (value == AppLocalizations.of(context).about) {
|
||||
showAbout(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
drawer: makeDrawer(context, 2),
|
||||
body: TabBarView(
|
||||
children: [
|
||||
// EXPENSE TAB
|
||||
SingleChildScrollView(
|
||||
child: Center(
|
||||
child: (selectedWallet == null)
|
||||
? const CircularProgressIndicator(
|
||||
strokeWidth: 5,
|
||||
)
|
||||
: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context).monthly,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Switch.adaptive(
|
||||
value: yearly,
|
||||
onChanged: (v) async {
|
||||
yearly = v;
|
||||
final s =
|
||||
await SharedPreferences.getInstance();
|
||||
chartType = yearly
|
||||
? (s.getInt("yearlygraph") ?? 1)
|
||||
: (s.getInt("monthlygraph") ?? 2);
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
Text(
|
||||
AppLocalizations.of(context).yearly,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: (MediaQuery.of(context)
|
||||
.platformBrightness ==
|
||||
Brightness.light)
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
spreadRadius: 3,
|
||||
blurRadius: 7,
|
||||
offset: const Offset(
|
||||
0,
|
||||
3,
|
||||
),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
color: (MediaQuery.of(context)
|
||||
.platformBrightness ==
|
||||
Brightness.dark)
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
yearly
|
||||
? AppLocalizations.of(context)
|
||||
.expensesPerYear(
|
||||
_selectedDate.year,
|
||||
)
|
||||
: AppLocalizations.of(context)
|
||||
.expensesPerMonth(
|
||||
DateFormat.yMMMM(locale)
|
||||
.format(_selectedDate),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width *
|
||||
0.9,
|
||||
height:
|
||||
MediaQuery.of(context).size.height *
|
||||
0.35,
|
||||
child: (chartType == null)
|
||||
? const CircularProgressIndicator()
|
||||
: (chartType == 1)
|
||||
? ExpensesBarChart(
|
||||
currency:
|
||||
selectedWallet!.currency,
|
||||
date: _selectedDate,
|
||||
locale: locale ?? "en",
|
||||
yearly: yearly,
|
||||
expenseData:
|
||||
generateChartData(
|
||||
EntryType.expense,
|
||||
),
|
||||
incomeData: const [],
|
||||
)
|
||||
: Padding(
|
||||
padding:
|
||||
const EdgeInsets.all(8),
|
||||
child: ExpensesLineChart(
|
||||
currency: selectedWallet!
|
||||
.currency,
|
||||
date: _selectedDate,
|
||||
locale: locale ?? "en",
|
||||
yearly: yearly,
|
||||
expenseData:
|
||||
generateChartData(
|
||||
EntryType.expense,
|
||||
),
|
||||
incomeData: const [],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 25,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: (MediaQuery.of(context)
|
||||
.platformBrightness ==
|
||||
Brightness.light)
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
spreadRadius: 3,
|
||||
blurRadius: 7,
|
||||
offset: const Offset(
|
||||
0,
|
||||
3,
|
||||
),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
color: (MediaQuery.of(context)
|
||||
.platformBrightness ==
|
||||
Brightness.dark)
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
width: MediaQuery.of(context).size.width * 0.95,
|
||||
height: MediaQuery.of(context).size.height * 0.4,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
textAlign: TextAlign.center,
|
||||
yearly
|
||||
? AppLocalizations.of(context)
|
||||
.expensesPerYearCategory(
|
||||
_selectedDate.year,
|
||||
)
|
||||
: AppLocalizations.of(context)
|
||||
.expensesPerMonthCategory(
|
||||
DateFormat.yMMMM(locale)
|
||||
.format(_selectedDate),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: CategoriesPieChart(
|
||||
// TODO: better size adaptivity without overflow
|
||||
locale: locale ?? "en",
|
||||
symbol: selectedWallet!.currency.symbol,
|
||||
entries: selectedWallet!.entries
|
||||
.where(
|
||||
(element) =>
|
||||
((!yearly)
|
||||
? element.date.month ==
|
||||
_selectedDate
|
||||
.month &&
|
||||
element.date.year ==
|
||||
_selectedDate.year
|
||||
: element.date.year ==
|
||||
_selectedDate.year) &&
|
||||
element.type ==
|
||||
EntryType.expense,
|
||||
)
|
||||
.toList(),
|
||||
categories: selectedWallet!.categories,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
), // Expense Tab END
|
||||
SingleChildScrollView(
|
||||
child: Center(
|
||||
child: (selectedWallet == null)
|
||||
? const CircularProgressIndicator(
|
||||
strokeWidth: 5,
|
||||
)
|
||||
: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context).monthly,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Switch.adaptive(
|
||||
value: yearly,
|
||||
onChanged: (v) async {
|
||||
yearly = v;
|
||||
final s =
|
||||
await SharedPreferences.getInstance();
|
||||
chartType = yearly
|
||||
? (s.getInt("yearlygraph") ?? 1)
|
||||
: (s.getInt("monthlygraph") ?? 2);
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
Text(
|
||||
AppLocalizations.of(context).yearly,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: (MediaQuery.of(context)
|
||||
.platformBrightness ==
|
||||
Brightness.light)
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
spreadRadius: 3,
|
||||
blurRadius: 7,
|
||||
offset: const Offset(
|
||||
0,
|
||||
3,
|
||||
),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
color: (MediaQuery.of(context)
|
||||
.platformBrightness ==
|
||||
Brightness.dark)
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
yearly
|
||||
? AppLocalizations.of(context)
|
||||
.incomePerYear(
|
||||
_selectedDate.year,
|
||||
)
|
||||
: AppLocalizations.of(context)
|
||||
.incomePerMonth(
|
||||
DateFormat.yMMMM(locale)
|
||||
.format(_selectedDate),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width *
|
||||
0.9,
|
||||
height:
|
||||
MediaQuery.of(context).size.height *
|
||||
0.35,
|
||||
child: (chartType == null)
|
||||
? const CircularProgressIndicator()
|
||||
: (chartType == 1)
|
||||
? ExpensesBarChart(
|
||||
currency:
|
||||
selectedWallet!.currency,
|
||||
date: _selectedDate,
|
||||
locale: locale ?? "en",
|
||||
yearly: yearly,
|
||||
expenseData: const [],
|
||||
incomeData: generateChartData(
|
||||
EntryType.income,
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding:
|
||||
const EdgeInsets.all(8),
|
||||
child: ExpensesLineChart(
|
||||
currency: selectedWallet!
|
||||
.currency,
|
||||
date: _selectedDate,
|
||||
locale: locale ?? "en",
|
||||
yearly: yearly,
|
||||
expenseData: const [],
|
||||
incomeData:
|
||||
generateChartData(
|
||||
EntryType.income,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 25,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: (MediaQuery.of(context)
|
||||
.platformBrightness ==
|
||||
Brightness.light)
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
spreadRadius: 3,
|
||||
blurRadius: 7,
|
||||
offset: const Offset(
|
||||
0,
|
||||
3,
|
||||
),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
color: (MediaQuery.of(context)
|
||||
.platformBrightness ==
|
||||
Brightness.dark)
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
width: MediaQuery.of(context).size.width * 0.95,
|
||||
height: MediaQuery.of(context).size.height * 0.4,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
yearly
|
||||
? AppLocalizations.of(context)
|
||||
.incomePerYearCategory(
|
||||
_selectedDate.year,
|
||||
)
|
||||
: AppLocalizations.of(context)
|
||||
.incomePerMonthCategory(
|
||||
DateFormat.yMMMM(locale)
|
||||
.format(_selectedDate),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: CategoriesPieChart(
|
||||
locale: locale ?? "en",
|
||||
symbol: selectedWallet!.currency.symbol,
|
||||
entries: selectedWallet!.entries
|
||||
.where(
|
||||
(element) =>
|
||||
((!yearly)
|
||||
? element.date.month ==
|
||||
_selectedDate
|
||||
.month &&
|
||||
element.date.year ==
|
||||
_selectedDate.year
|
||||
: element.date.year ==
|
||||
_selectedDate.year) &&
|
||||
element.type ==
|
||||
EntryType.income,
|
||||
)
|
||||
.toList(),
|
||||
categories: selectedWallet!.categories,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
), // Income Tab END
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
688
lib/views/graphs/graph_view.dart
Normal file
688
lib/views/graphs/graph_view.dart
Normal file
|
@ -0,0 +1,688 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:prasule/api/category.dart';
|
||||
import 'package:prasule/api/wallet.dart';
|
||||
import 'package:prasule/api/wallet_manager.dart';
|
||||
import 'package:prasule/main.dart';
|
||||
import 'package:prasule/pw/platformroute.dart';
|
||||
import 'package:prasule/util/drawer.dart';
|
||||
import 'package:prasule/util/graphs.dart';
|
||||
import 'package:prasule/util/utils.dart';
|
||||
import 'package:prasule/views/settings/settings.dart';
|
||||
import 'package:prasule/views/setup.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:wheel_chooser/wheel_chooser.dart';
|
||||
|
||||
/// Shows data from a [Wallet] in graphs
|
||||
class GraphView extends StatefulWidget {
|
||||
/// Shows data from a [Wallet] in graphs
|
||||
const GraphView({super.key});
|
||||
|
||||
@override
|
||||
State<GraphView> createState() => _GraphViewState();
|
||||
}
|
||||
|
||||
class _GraphViewState extends State<GraphView> {
|
||||
var _selectedDate = DateTime.now();
|
||||
List<Wallet> wallets = [];
|
||||
String? locale;
|
||||
bool yearly = true;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
locale ??= Localizations.localeOf(context).languageCode;
|
||||
}
|
||||
|
||||
List<double> generateChartData(EntryType type) {
|
||||
final d = _selectedDate.add(const Duration(days: 31));
|
||||
final data = List<double>.filled(
|
||||
yearly ? 12 : DateTime(d.year, d.month, 0).day,
|
||||
0,
|
||||
);
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
final entriesForRange = WalletManager.selectedWallet.entries.where(
|
||||
(element) =>
|
||||
((!yearly)
|
||||
? element.date.month == _selectedDate.month &&
|
||||
element.date.year == _selectedDate.year &&
|
||||
element.date.day == i + 1
|
||||
: element.date.month == i + 1 &&
|
||||
element.date.year == _selectedDate.year) &&
|
||||
element.type == type,
|
||||
);
|
||||
var sum = 0.0;
|
||||
for (final e in entriesForRange) {
|
||||
sum += e.data.amount;
|
||||
}
|
||||
data[i] = sum;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
final availableYears = <WheelChoice<int>>[];
|
||||
|
||||
void loadWallet() {
|
||||
wallets = WalletManager.listWallets();
|
||||
if (wallets.isEmpty && mounted) {
|
||||
unawaited(
|
||||
Navigator.of(context)
|
||||
.pushReplacement(platformRoute((c) => const SetupView())),
|
||||
);
|
||||
return;
|
||||
}
|
||||
WalletManager.selectedWallet = wallets.first;
|
||||
availableYears.clear();
|
||||
for (final entry in WalletManager.selectedWallet.entries) {
|
||||
if (!availableYears.any((element) => element.value == entry.date.year)) {
|
||||
availableYears.add(
|
||||
WheelChoice<int>(
|
||||
value: entry.date.year,
|
||||
title: entry.date.year.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
int? chartType;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
loadWallet();
|
||||
SharedPreferences.getInstance().then((s) {
|
||||
chartType = s.getInt("yearlygraph") ?? 1;
|
||||
logger.d(chartType);
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: 2,
|
||||
child: Scaffold(
|
||||
floatingActionButton: Tooltip(
|
||||
message: AppLocalizations.of(context).changeDate,
|
||||
child: FloatingActionButton(
|
||||
child: const Icon(Icons.calendar_month),
|
||||
onPressed: () async {
|
||||
var selectedYear = _selectedDate.year;
|
||||
var selectedMonth = _selectedDate.month;
|
||||
await showAdaptiveDialog<void>(
|
||||
context: context,
|
||||
builder: (c) => AlertDialog.adaptive(
|
||||
title: Text(
|
||||
yearly
|
||||
? AppLocalizations.of(context).selectYear
|
||||
: AppLocalizations.of(context).selectMonth,
|
||||
),
|
||||
content: LimitedBox(
|
||||
maxHeight: MediaQuery.of(context).size.width * 0.7,
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 5,
|
||||
children: [
|
||||
if (!yearly)
|
||||
SizedBox(
|
||||
width: 120,
|
||||
height: 100,
|
||||
child: WheelChooser<int>.choices(
|
||||
onChoiceChanged: (v) {
|
||||
selectedMonth = v as int;
|
||||
},
|
||||
startPosition: _selectedDate.month - 1,
|
||||
choices: List<WheelChoice<int>>.generate(
|
||||
12,
|
||||
(index) => WheelChoice(
|
||||
value: index + 1,
|
||||
title: DateFormat.MMMM(locale ?? "en").format(
|
||||
DateTime(
|
||||
_selectedDate.year,
|
||||
index + 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 100,
|
||||
width: 80,
|
||||
child: WheelChooser<int>.choices(
|
||||
startPosition: availableYears.indexWhere(
|
||||
(element) => element.value == _selectedDate.year,
|
||||
),
|
||||
onChoiceChanged: (v) {
|
||||
selectedYear = v as int;
|
||||
},
|
||||
choices: availableYears,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_selectedDate = DateTime(selectedYear, selectedMonth);
|
||||
Navigator.of(c).pop();
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
appBar: AppBar(
|
||||
bottom: TabBar(
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Text(AppLocalizations.of(context).expenses),
|
||||
),
|
||||
Tab(
|
||||
child: Text(AppLocalizations.of(context).incomePlural),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: DropdownButton<int>(
|
||||
value: wallets.indexOf(
|
||||
wallets
|
||||
.where((w) => w.name == WalletManager.selectedWallet.name)
|
||||
.first,
|
||||
),
|
||||
items: [
|
||||
...wallets.map(
|
||||
(e) => DropdownMenuItem(
|
||||
value: wallets.indexOf(
|
||||
e,
|
||||
),
|
||||
child: Text(e.name),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: -1,
|
||||
child: Text(AppLocalizations.of(context).newWallet),
|
||||
),
|
||||
],
|
||||
onChanged: (v) async {
|
||||
if (v == null || v == -1) {
|
||||
await Navigator.of(context).push(
|
||||
platformRoute(
|
||||
(c) => const SetupView(
|
||||
newWallet: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
wallets = WalletManager.listWallets();
|
||||
logger.i(wallets.length);
|
||||
WalletManager.selectedWallet = wallets.last;
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
WalletManager.selectedWallet = wallets[v];
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
AppLocalizations.of(context).settings,
|
||||
AppLocalizations.of(context).about,
|
||||
].map((e) => PopupMenuItem(value: e, child: Text(e))).toList(),
|
||||
onSelected: (value) {
|
||||
if (value == AppLocalizations.of(context).settings) {
|
||||
Navigator.of(context)
|
||||
.push(
|
||||
platformRoute(
|
||||
(context) => const SettingsView(),
|
||||
),
|
||||
)
|
||||
.then((value) async {
|
||||
WalletManager.selectedWallet = WalletManager.loadWallet(
|
||||
WalletManager.selectedWallet.name);
|
||||
final s = await SharedPreferences.getInstance();
|
||||
chartType = s.getInt("monthlygraph") ?? 2;
|
||||
setState(() {});
|
||||
});
|
||||
} else if (value == AppLocalizations.of(context).about) {
|
||||
showAbout(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
drawer: makeDrawer(context, Pages.graphs),
|
||||
body: TabBarView(
|
||||
children: [
|
||||
// EXPENSE TAB
|
||||
SingleChildScrollView(
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context).monthly,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Switch.adaptive(
|
||||
value: yearly,
|
||||
onChanged: (v) async {
|
||||
yearly = v;
|
||||
final s = await SharedPreferences.getInstance();
|
||||
chartType = yearly
|
||||
? (s.getInt("yearlygraph") ?? 1)
|
||||
: (s.getInt("monthlygraph") ?? 2);
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
Text(
|
||||
AppLocalizations.of(context).yearly,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow:
|
||||
(MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.light)
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
spreadRadius: 3,
|
||||
blurRadius: 7,
|
||||
offset: const Offset(
|
||||
0,
|
||||
3,
|
||||
),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
color: (MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.dark)
|
||||
? Theme.of(context).colorScheme.secondaryContainer
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
yearly
|
||||
? AppLocalizations.of(context)
|
||||
.expensesPerYear(
|
||||
_selectedDate.year,
|
||||
)
|
||||
: AppLocalizations.of(context)
|
||||
.expensesPerMonth(
|
||||
DateFormat.yMMMM(locale)
|
||||
.format(_selectedDate),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
height:
|
||||
MediaQuery.of(context).size.height * 0.35,
|
||||
child: (chartType == null)
|
||||
? const CircularProgressIndicator()
|
||||
: (chartType == 1)
|
||||
? ExpensesBarChart(
|
||||
currency: WalletManager
|
||||
.selectedWallet.currency,
|
||||
date: _selectedDate,
|
||||
locale: locale ?? "en",
|
||||
yearly: yearly,
|
||||
expenseData: generateChartData(
|
||||
EntryType.expense,
|
||||
),
|
||||
incomeData: const [],
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ExpensesLineChart(
|
||||
currency: WalletManager
|
||||
.selectedWallet.currency,
|
||||
date: _selectedDate,
|
||||
locale: locale ?? "en",
|
||||
yearly: yearly,
|
||||
expenseData: generateChartData(
|
||||
EntryType.expense,
|
||||
),
|
||||
incomeData: const [],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 25,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow:
|
||||
(MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.light)
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
spreadRadius: 3,
|
||||
blurRadius: 7,
|
||||
offset: const Offset(
|
||||
0,
|
||||
3,
|
||||
),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
color: (MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.dark)
|
||||
? Theme.of(context).colorScheme.secondaryContainer
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
width: MediaQuery.of(context).size.width * 0.95,
|
||||
height: MediaQuery.of(context).size.height * 0.4,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
textAlign: TextAlign.center,
|
||||
yearly
|
||||
? AppLocalizations.of(context)
|
||||
.expensesPerYearCategory(
|
||||
_selectedDate.year,
|
||||
)
|
||||
: AppLocalizations.of(context)
|
||||
.expensesPerMonthCategory(
|
||||
DateFormat.yMMMM(locale)
|
||||
.format(_selectedDate),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: CategoriesPieChart(
|
||||
// TODO: better size adaptivity without overflow
|
||||
locale: locale ?? "en",
|
||||
symbol: WalletManager
|
||||
.selectedWallet.currency.symbol,
|
||||
entries: WalletManager.selectedWallet.entries
|
||||
.where(
|
||||
(element) =>
|
||||
((!yearly)
|
||||
? element.date.month ==
|
||||
_selectedDate.month &&
|
||||
element.date.year ==
|
||||
_selectedDate.year
|
||||
: element.date.year ==
|
||||
_selectedDate.year) &&
|
||||
element.type == EntryType.expense,
|
||||
)
|
||||
.toList(),
|
||||
categories:
|
||||
WalletManager.selectedWallet.categories,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
), // Expense Tab END
|
||||
SingleChildScrollView(
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context).monthly,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Switch.adaptive(
|
||||
value: yearly,
|
||||
onChanged: (v) async {
|
||||
yearly = v;
|
||||
final s = await SharedPreferences.getInstance();
|
||||
chartType = yearly
|
||||
? (s.getInt("yearlygraph") ?? 1)
|
||||
: (s.getInt("monthlygraph") ?? 2);
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
Text(
|
||||
AppLocalizations.of(context).yearly,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow:
|
||||
(MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.light)
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
spreadRadius: 3,
|
||||
blurRadius: 7,
|
||||
offset: const Offset(
|
||||
0,
|
||||
3,
|
||||
),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
color: (MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.dark)
|
||||
? Theme.of(context).colorScheme.secondaryContainer
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
yearly
|
||||
? AppLocalizations.of(context)
|
||||
.incomePerYear(
|
||||
_selectedDate.year,
|
||||
)
|
||||
: AppLocalizations.of(context)
|
||||
.incomePerMonth(
|
||||
DateFormat.yMMMM(locale)
|
||||
.format(_selectedDate),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
height:
|
||||
MediaQuery.of(context).size.height * 0.35,
|
||||
child: (chartType == null)
|
||||
? const CircularProgressIndicator()
|
||||
: (chartType == 1)
|
||||
? ExpensesBarChart(
|
||||
currency: WalletManager
|
||||
.selectedWallet.currency,
|
||||
date: _selectedDate,
|
||||
locale: locale ?? "en",
|
||||
yearly: yearly,
|
||||
expenseData: const [],
|
||||
incomeData: generateChartData(
|
||||
EntryType.income,
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ExpensesLineChart(
|
||||
currency: WalletManager
|
||||
.selectedWallet.currency,
|
||||
date: _selectedDate,
|
||||
locale: locale ?? "en",
|
||||
yearly: yearly,
|
||||
expenseData: const [],
|
||||
incomeData: generateChartData(
|
||||
EntryType.income,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 25,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow:
|
||||
(MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.light)
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
spreadRadius: 3,
|
||||
blurRadius: 7,
|
||||
offset: const Offset(
|
||||
0,
|
||||
3,
|
||||
),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
color: (MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.dark)
|
||||
? Theme.of(context).colorScheme.secondaryContainer
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
width: MediaQuery.of(context).size.width * 0.95,
|
||||
height: MediaQuery.of(context).size.height * 0.4,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
yearly
|
||||
? AppLocalizations.of(context)
|
||||
.incomePerYearCategory(
|
||||
_selectedDate.year,
|
||||
)
|
||||
: AppLocalizations.of(context)
|
||||
.incomePerMonthCategory(
|
||||
DateFormat.yMMMM(locale)
|
||||
.format(_selectedDate),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: CategoriesPieChart(
|
||||
locale: locale ?? "en",
|
||||
symbol: WalletManager
|
||||
.selectedWallet.currency.symbol,
|
||||
entries: WalletManager.selectedWallet.entries
|
||||
.where(
|
||||
(element) =>
|
||||
((!yearly)
|
||||
? element.date.month ==
|
||||
_selectedDate.month &&
|
||||
element.date.year ==
|
||||
_selectedDate.year
|
||||
: element.date.year ==
|
||||
_selectedDate.year) &&
|
||||
element.type == EntryType.income,
|
||||
)
|
||||
.toList(),
|
||||
categories:
|
||||
WalletManager.selectedWallet.categories,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
), // Income Tab END
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -29,7 +29,7 @@ import 'package:prasule/util/drawer.dart';
|
|||
import 'package:prasule/util/sorting.dart';
|
||||
import 'package:prasule/util/text_color.dart';
|
||||
import 'package:prasule/util/utils.dart';
|
||||
import 'package:prasule/views/create_entry.dart';
|
||||
import 'package:prasule/views/entry/create_entry.dart';
|
||||
import 'package:prasule/views/settings/settings.dart';
|
||||
import 'package:prasule/views/settings/tessdata_list.dart';
|
||||
import 'package:prasule/views/setup.dart';
|
||||
|
@ -94,8 +94,10 @@ class _HomeViewState extends State<HomeView> {
|
|||
setState(() {});
|
||||
},
|
||||
child: Scaffold(
|
||||
drawer: makeDrawer(context, 1),
|
||||
drawer: makeDrawer(context, Pages.home),
|
||||
floatingActionButton: SpeedDial(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
|
|
|
@ -31,6 +31,7 @@ class _InitializationScreenState extends State<InitializationScreen> {
|
|||
.pushReplacement(platformRoute((c) => const SetupView()));
|
||||
return;
|
||||
}
|
||||
WalletManager.selectedWallet = wallets.first;
|
||||
Navigator.of(context)
|
||||
.pushReplacement(platformRoute((c) => const HomeView()));
|
||||
});
|
||||
|
|
|
@ -17,7 +17,7 @@ import 'package:prasule/pw/platformroute.dart';
|
|||
import 'package:prasule/util/drawer.dart';
|
||||
import 'package:prasule/util/text_color.dart';
|
||||
import 'package:prasule/util/utils.dart';
|
||||
import 'package:prasule/views/create_recur_entry.dart';
|
||||
import 'package:prasule/views/recurring/create_recur_entry.dart';
|
||||
import 'package:prasule/views/settings/settings.dart';
|
||||
import 'package:prasule/views/setup.dart';
|
||||
|
||||
|
@ -64,7 +64,7 @@ class _RecurringEntriesViewState extends State<RecurringEntriesView> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
drawer: makeDrawer(context, 3),
|
||||
drawer: makeDrawer(context, Pages.recurringEntries),
|
||||
appBar: AppBar(
|
||||
title: DropdownButton<int>(
|
||||
value: (selectedWallet == null)
|
|
@ -167,6 +167,9 @@ class _SetupViewState extends State<SetupView> {
|
|||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
|
||||
WalletManager.selectedWallet = wallet;
|
||||
|
||||
if (!context.mounted) return;
|
||||
unawaited(
|
||||
Navigator.of(context).pushReplacement(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
sonar.projectKey=prasule
|
||||
sonar.sources=lib,pubspec.yaml
|
||||
sonar.tests=test
|
||||
sonar.dart.analyzer.mode=MANUAL
|
||||
sonar.tests=integration_test,test_driver
|
||||
sonar.dart.analyzer.mode=FLUTTER
|
||||
sonar.dart.analyzer.options.override=true
|
||||
sonar.host.url=https://sq.mnau.xyz
|
Loading…
Reference in a new issue