feat: creating debt scenarios

also changed way of loading selected wallet
This commit is contained in:
Matyáš Caras 2024-07-19 17:34:45 +02:00
parent 300f359070
commit ef52caa836
Signed by: hernik
GPG key ID: 2A3175F98820C5C6
20 changed files with 946 additions and 478 deletions

View file

@ -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

View file

@ -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()];

View file

@ -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(),
);
}

View file

@ -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;
}

View file

@ -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,
};

View file

@ -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()];

View file

@ -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();

View file

@ -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,
};

View file

@ -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
///

View file

@ -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"
}

View file

@ -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"
}

View file

@ -10,26 +10,26 @@ 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,
this.controller,
this.enabled,
this.labelText,
this.obscureText = false,
this.autocorrect = false,
this.keyboardType,
this.inputFormatters = const [],
this.onChanged,
this.autofillHints,
this.textStyle,
this.textAlign = TextAlign.start,
this.maxLines = 1,
this.focusNode,
this.inputBorder = const OutlineInputBorder(),
this.suffix,
this.prefix,
});
class PlatformField extends PlatformWidget<TextFormField, CupertinoTextField> {
const PlatformField(
{super.key,
this.controller,
this.enabled,
this.labelText,
this.obscureText = false,
this.autocorrect = false,
this.keyboardType,
this.inputFormatters = const [],
this.onChanged,
this.autofillHints,
this.textStyle,
this.textAlign = TextAlign.start,
this.maxLines = 1,
this.focusNode,
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

View file

@ -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/debts/debt_view.dart';
import 'package:prasule/views/graphs/graph_view.dart';
import 'package:prasule/views/home.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
}

View 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(),
),
),
),
);
}
}

View 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),
),
],
),
),
),
),
),
);
}
}

View file

@ -31,7 +31,6 @@ class GraphView extends StatefulWidget {
class _GraphViewState extends State<GraphView> {
var _selectedDate = DateTime.now();
Wallet? selectedWallet;
List<Wallet> wallets = [];
String? locale;
bool yearly = true;
@ -48,9 +47,8 @@ class _GraphViewState extends State<GraphView> {
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(
final entriesForRange = WalletManager.selectedWallet.entries.where(
(element) =>
((!yearly)
? element.date.month == _selectedDate.month &&
@ -80,9 +78,9 @@ class _GraphViewState extends State<GraphView> {
);
return;
}
selectedWallet = wallets.first;
WalletManager.selectedWallet = wallets.first;
availableYears.clear();
for (final entry in selectedWallet!.entries) {
for (final entry in WalletManager.selectedWallet.entries) {
if (!availableYears.any((element) => element.value == entry.date.year)) {
availableYears.add(
WheelChoice<int>(
@ -200,11 +198,11 @@ class _GraphViewState extends State<GraphView> {
],
),
title: DropdownButton<int>(
value: (selectedWallet == null)
? -1
: wallets.indexOf(
wallets.where((w) => w.name == selectedWallet!.name).first,
),
value: wallets.indexOf(
wallets
.where((w) => w.name == WalletManager.selectedWallet.name)
.first,
),
items: [
...wallets.map(
(e) => DropdownMenuItem(
@ -230,11 +228,11 @@ class _GraphViewState extends State<GraphView> {
);
wallets = WalletManager.listWallets();
logger.i(wallets.length);
selectedWallet = wallets.last;
WalletManager.selectedWallet = wallets.last;
setState(() {});
return;
}
selectedWallet = wallets[v];
WalletManager.selectedWallet = wallets[v];
setState(() {});
},
),
@ -253,8 +251,8 @@ class _GraphViewState extends State<GraphView> {
),
)
.then((value) async {
selectedWallet =
WalletManager.loadWallet(selectedWallet!.name);
WalletManager.selectedWallet = WalletManager.loadWallet(
WalletManager.selectedWallet.name);
final s = await SharedPreferences.getInstance();
chartType = s.getInt("monthlygraph") ?? 2;
setState(() {});
@ -266,453 +264,420 @@ class _GraphViewState extends State<GraphView> {
),
],
),
drawer: makeDrawer(context, 2),
drawer: makeDrawer(context, Pages.graphs),
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,
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: [
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);
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,
),
),
],
),
setState(() {});
},
),
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,
),
),
],
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: (selectedWallet == null)
? const CircularProgressIndicator(
strokeWidth: 5,
)
: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Column(
mainAxisAlignment: MainAxisAlignment.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: [
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);
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,
),
),
],
),
setState(() {});
},
),
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,
),
),
],
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
],

View file

@ -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)),
),

View file

@ -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()));
});

View file

@ -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)

View file

@ -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(