parent
6163a95849
commit
17a3a1ce20
19 changed files with 921 additions and 26 deletions
|
@ -4,6 +4,8 @@
|
|||
- Create a default "no category" category, mainly to store entries with removed categories
|
||||
- Categories now have changeable colors assigned to them
|
||||
- Added pie chart for expense/income data per category
|
||||
- Added recurring entries
|
||||
- Fixed wrong default sorting
|
||||
# 1.0.0-alpha+2
|
||||
- Fixed localization issues
|
||||
- Added graphs for expenses and income per month/year
|
||||
|
|
52
lib/api/recurring_entry.dart
Normal file
52
lib/api/recurring_entry.dart
Normal file
|
@ -0,0 +1,52 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:prasule/api/category.dart';
|
||||
import 'package:prasule/api/entry_data.dart';
|
||||
import 'package:prasule/api/wallet_entry.dart';
|
||||
|
||||
part 'recurring_entry.g.dart';
|
||||
|
||||
/// This is a [WalletSingleEntry] that is automatically recurring
|
||||
@JsonSerializable()
|
||||
class RecurringWalletEntry extends WalletSingleEntry {
|
||||
/// This is a [WalletSingleEntry] that is automatically recurring
|
||||
RecurringWalletEntry({
|
||||
required super.data,
|
||||
required super.type,
|
||||
required super.date,
|
||||
required super.category,
|
||||
required super.id,
|
||||
required this.lastRunDate,
|
||||
required this.recurType,
|
||||
this.repeatAfter = 1,
|
||||
});
|
||||
|
||||
/// Connects generated fromJson method
|
||||
factory RecurringWalletEntry.fromJson(Map<String, dynamic> json) =>
|
||||
_$RecurringWalletEntryFromJson(json);
|
||||
|
||||
/// Connects generated toJson method
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$RecurringWalletEntryToJson(this);
|
||||
|
||||
/// Last date the recurring entry was added into the single entry list
|
||||
DateTime lastRunDate;
|
||||
|
||||
/// After how many {recurType} should the entry recur
|
||||
int repeatAfter;
|
||||
|
||||
/// What type of recurrence should happen
|
||||
RecurType recurType;
|
||||
}
|
||||
|
||||
/// How a [RecurringWalletEntry] should recur
|
||||
@JsonEnum()
|
||||
enum RecurType {
|
||||
/// Will recur every {repeatAfter} months
|
||||
month,
|
||||
|
||||
/// Will recur every {repeatAfter} years
|
||||
year,
|
||||
|
||||
/// Will recur every {repeatAfter} days
|
||||
day
|
||||
}
|
45
lib/api/recurring_entry.g.dart
Normal file
45
lib/api/recurring_entry.g.dart
Normal file
|
@ -0,0 +1,45 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'recurring_entry.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
RecurringWalletEntry _$RecurringWalletEntryFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
RecurringWalletEntry(
|
||||
data: EntryData.fromJson(json['data'] as Map<String, dynamic>),
|
||||
type: $enumDecode(_$EntryTypeEnumMap, json['type']),
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
category:
|
||||
WalletCategory.fromJson(json['category'] as Map<String, dynamic>),
|
||||
id: json['id'] as int,
|
||||
lastRunDate: DateTime.parse(json['lastRunDate'] as String),
|
||||
repeatAfter: json['repeatAfter'] as int,
|
||||
recurType: $enumDecode(_$RecurTypeEnumMap, json['recurType']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RecurringWalletEntryToJson(
|
||||
RecurringWalletEntry instance) =>
|
||||
<String, dynamic>{
|
||||
'type': _$EntryTypeEnumMap[instance.type]!,
|
||||
'data': instance.data,
|
||||
'date': instance.date.toIso8601String(),
|
||||
'category': instance.category,
|
||||
'id': instance.id,
|
||||
'lastRunDate': instance.lastRunDate.toIso8601String(),
|
||||
'repeatAfter': instance.repeatAfter,
|
||||
'recurType': _$RecurTypeEnumMap[instance.recurType]!,
|
||||
};
|
||||
|
||||
const _$EntryTypeEnumMap = {
|
||||
EntryType.expense: 'expense',
|
||||
EntryType.income: 'income',
|
||||
};
|
||||
|
||||
const _$RecurTypeEnumMap = {
|
||||
RecurType.month: 'month',
|
||||
RecurType.year: 'year',
|
||||
RecurType.day: 'day',
|
||||
};
|
|
@ -1,8 +1,11 @@
|
|||
import 'package:currency_picker/currency_picker.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:prasule/api/category.dart';
|
||||
import 'package:prasule/api/walletentry.dart';
|
||||
import 'package:prasule/api/walletmanager.dart';
|
||||
import 'package:prasule/api/recurring_entry.dart';
|
||||
import 'package:prasule/api/wallet_entry.dart';
|
||||
import 'package:prasule/api/wallet_manager.dart';
|
||||
import 'package:prasule/main.dart';
|
||||
part 'wallet.g.dart';
|
||||
|
||||
Currency _currencyFromJson(Map<String, dynamic> data) =>
|
||||
|
@ -19,12 +22,16 @@ class Wallet {
|
|||
required this.currency,
|
||||
this.categories = const [],
|
||||
this.entries = const [],
|
||||
this.recurringEntries = const [],
|
||||
this.starterBalance = 0,
|
||||
});
|
||||
|
||||
/// Connects generated fromJson method
|
||||
factory Wallet.fromJson(Map<String, dynamic> json) => _$WalletFromJson(json);
|
||||
|
||||
/// A list of all [RecurringWalletEntry]s
|
||||
final List<RecurringWalletEntry> recurringEntries;
|
||||
|
||||
/// Name of the wallet
|
||||
final String name;
|
||||
|
||||
|
@ -65,6 +72,56 @@ class Wallet {
|
|||
return id;
|
||||
}
|
||||
|
||||
/// Handles adding recurring entries to the entry list
|
||||
void recurEntries() {
|
||||
final n = DateTime.now();
|
||||
for (final ent in recurringEntries) {
|
||||
var m = DateTime(
|
||||
(ent.recurType == RecurType.year)
|
||||
? ent.lastRunDate.year + ent.repeatAfter
|
||||
: ent.lastRunDate.year,
|
||||
(ent.recurType == RecurType.month)
|
||||
? ent.lastRunDate.month + ent.repeatAfter
|
||||
: ent.lastRunDate.month,
|
||||
(ent.recurType == RecurType.day)
|
||||
? ent.lastRunDate.day + ent.repeatAfter
|
||||
: ent.lastRunDate.day,
|
||||
); // create the date after which we should recur
|
||||
|
||||
while (n.isAfter(
|
||||
m,
|
||||
)) {
|
||||
logger.i("Adding recurring entry ${ent.data.name}");
|
||||
recurringEntries[recurringEntries.indexOf(ent)].lastRunDate =
|
||||
m; // update date on recurring entry
|
||||
logger.i(recurringEntries[recurringEntries.indexOf(ent)].lastRunDate);
|
||||
final addedEntry = (recurringEntries[recurringEntries.indexOf(ent)]
|
||||
as WalletSingleEntry)
|
||||
..date = DateTime.now()
|
||||
..id = nextId; // copy entry with today's date and unique ID
|
||||
entries.add(
|
||||
addedEntry,
|
||||
); // add it to entries
|
||||
|
||||
m = DateTime(
|
||||
(ent.recurType == RecurType.year)
|
||||
? ent.lastRunDate.year + ent.repeatAfter
|
||||
: ent.lastRunDate.year,
|
||||
(ent.recurType == RecurType.month)
|
||||
? ent.lastRunDate.month + ent.repeatAfter
|
||||
: ent.lastRunDate.month,
|
||||
(ent.recurType == RecurType.day)
|
||||
? ent.lastRunDate.day + ent.repeatAfter
|
||||
: ent.lastRunDate.day,
|
||||
); // add tne variable again to check if we aren't missing any entries
|
||||
logger.i(
|
||||
"Last recurred date is now on ${DateFormat.yMMMMd().format(m)} (${n.isAfter(m)})");
|
||||
}
|
||||
|
||||
WalletManager.saveWallet(this); // save wallet
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the specified category.
|
||||
///
|
||||
/// All [WalletSingleEntry]s will have their category reassigned
|
||||
|
|
|
@ -18,10 +18,16 @@ Wallet _$WalletFromJson(Map<String, dynamic> json) => Wallet(
|
|||
(e) => WalletSingleEntry.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
recurringEntries: (json['recurringEntries'] as List<dynamic>?)
|
||||
?.map((e) =>
|
||||
RecurringWalletEntry.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
starterBalance: (json['starterBalance'] as num?)?.toDouble() ?? 0,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$WalletToJson(Wallet instance) => <String, dynamic>{
|
||||
'recurringEntries': instance.recurringEntries,
|
||||
'name': instance.name,
|
||||
'categories': instance.categories,
|
||||
'entries': instance.entries,
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'package:json_annotation/json_annotation.dart';
|
|||
import 'package:prasule/api/category.dart';
|
||||
import 'package:prasule/api/entry_data.dart';
|
||||
|
||||
part 'walletentry.g.dart';
|
||||
part 'wallet_entry.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'walletentry.dart';
|
||||
part of 'wallet_entry.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
|
@ -81,5 +81,11 @@
|
|||
"noCategory":"Žádná kategorie",
|
||||
"done":"Hotovo",
|
||||
"pickColor":"Zvolte barvu",
|
||||
"changeDate":"Změnit ze kterého měsíce/roku brát data"
|
||||
"changeDate":"Změnit ze kterého měsíce/roku brát data",
|
||||
"recurringPayments":"Opakující se platby",
|
||||
"monthCounter": "{count, plural, =1{měsíc} few{měsíce} many{měsíců} other{měsíců} }",
|
||||
"dayCounter":"{count, plural, =1{den} few{dny} many{dnů} other{dnů} }",
|
||||
"yearCounter":"{count, plural, =1{rok} few{rok} many{let} other{let} }",
|
||||
"recurEvery":"{count, plural, =1{Opakovat každý} few{Opakovat každé} many{Opakovat každých} other{Opakovat každých}}",
|
||||
"startingWithDate": "počínaje datem"
|
||||
}
|
|
@ -161,5 +161,47 @@
|
|||
"noCategory":"No category",
|
||||
"done":"Done",
|
||||
"pickColor":"Pick a color",
|
||||
"changeDate":"Change what month/year to pick data from"
|
||||
"changeDate":"Change what month/year to pick data from",
|
||||
"recurringPayments":"Recurring payments",
|
||||
"recurEvery":"{count, plural, other{Recur every}}",
|
||||
"@recurEvery":{
|
||||
"description": "Shown when creating recurring entries, ex.: Recur every 2 months",
|
||||
"placeholders": {
|
||||
"count":{
|
||||
"description": "Specifies how many X are being counted",
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"monthCounter":"{count, plural, =1{month} other{months} }",
|
||||
"@monthCounter":{
|
||||
"placeholders": {
|
||||
"count":{
|
||||
"description": "Specifies how many months are being counted",
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dayCounter":"{count, plural, =1{day} other{days} }",
|
||||
"@dayCounter":{
|
||||
"placeholders": {
|
||||
"count":{
|
||||
"description": "Specifies how many days are being counted",
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"yearCounter":"{count, plural, =1{year} other{years} }",
|
||||
"@yearCounter":{
|
||||
"placeholders": {
|
||||
"count":{
|
||||
"description": "Specifies how many years are being counted",
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"startingWithDate": "starting",
|
||||
"@startingWithDate":{
|
||||
"description": "Shown after 'Recur every X Y', e.g. 'Recur every 2 month starting 20th June 2023'"
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ 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/home.dart';
|
||||
import 'package:prasule/views/recurring_view.dart';
|
||||
|
||||
/// Makes the drawer because I won't enter the same code in every view
|
||||
Drawer makeDrawer(BuildContext context, int page) => Drawer(
|
||||
|
@ -39,6 +40,22 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer(
|
|||
.pushReplacement(platformRoute((p0) => const GraphView()));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.repeat),
|
||||
title: Text(
|
||||
AppLocalizations.of(context).recurringPayments,
|
||||
),
|
||||
selected: page == 3,
|
||||
onTap: () {
|
||||
if (page == 3) {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pushReplacement(
|
||||
platformRoute((p0) => const RecurringEntriesView()),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -5,8 +5,7 @@ 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/walletentry.dart';
|
||||
import 'package:prasule/main.dart';
|
||||
import 'package:prasule/api/wallet_entry.dart';
|
||||
import 'package:prasule/util/get_last_date.dart';
|
||||
import 'package:prasule/util/text_color.dart';
|
||||
|
||||
|
|
|
@ -4,15 +4,15 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|||
import 'package:prasule/api/category.dart';
|
||||
import 'package:prasule/api/entry_data.dart';
|
||||
import 'package:prasule/api/wallet.dart';
|
||||
import 'package:prasule/api/walletentry.dart';
|
||||
import 'package:prasule/api/walletmanager.dart';
|
||||
import 'package:prasule/api/wallet_entry.dart';
|
||||
import 'package:prasule/api/wallet_manager.dart';
|
||||
import 'package:prasule/pw/platformbutton.dart';
|
||||
import 'package:prasule/pw/platformfield.dart';
|
||||
|
||||
/// Used when user wants to add new entry
|
||||
class CreateEntryView extends StatefulWidget {
|
||||
class CreateSingleEntryView extends StatefulWidget {
|
||||
/// Used when user wants to add new entry
|
||||
const CreateEntryView({required this.w, super.key, this.editEntry});
|
||||
const CreateSingleEntryView({required this.w, super.key, this.editEntry});
|
||||
|
||||
/// The wallet, where the entry will be saved to
|
||||
final Wallet w;
|
||||
|
@ -23,10 +23,10 @@ class CreateEntryView extends StatefulWidget {
|
|||
final WalletSingleEntry? editEntry;
|
||||
|
||||
@override
|
||||
State<CreateEntryView> createState() => _CreateEntryViewState();
|
||||
State createState() => _CreateSingleEntryViewState();
|
||||
}
|
||||
|
||||
class _CreateEntryViewState extends State<CreateEntryView> {
|
||||
class _CreateSingleEntryViewState extends State<CreateSingleEntryView> {
|
||||
late WalletSingleEntry newEntry;
|
||||
@override
|
||||
void initState() {
|
||||
|
|
342
lib/views/create_recur_entry.dart
Normal file
342
lib/views/create_recur_entry.dart
Normal file
|
@ -0,0 +1,342 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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/entry_data.dart';
|
||||
import 'package:prasule/api/recurring_entry.dart';
|
||||
import 'package:prasule/api/wallet.dart';
|
||||
import 'package:prasule/api/wallet_manager.dart';
|
||||
import 'package:prasule/main.dart';
|
||||
import 'package:prasule/pw/platformbutton.dart';
|
||||
import 'package:prasule/pw/platformfield.dart';
|
||||
|
||||
/// Used when user wants to add new entry
|
||||
class CreateRecurringEntryView extends StatefulWidget {
|
||||
/// Used when user wants to add new entry
|
||||
const CreateRecurringEntryView({
|
||||
required this.w,
|
||||
required this.locale,
|
||||
super.key,
|
||||
this.editEntry,
|
||||
});
|
||||
|
||||
/// The wallet, where the entry will be saved to
|
||||
final Wallet w;
|
||||
|
||||
/// Entry we want to edit
|
||||
///
|
||||
/// Is null unless we are editing an existing entry
|
||||
final RecurringWalletEntry? editEntry;
|
||||
|
||||
/// Selected locale
|
||||
final String locale;
|
||||
|
||||
@override
|
||||
State createState() => _CreateRecurringEntryViewState();
|
||||
}
|
||||
|
||||
class _CreateRecurringEntryViewState extends State<CreateRecurringEntryView> {
|
||||
late RecurringWalletEntry newEntry;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.editEntry != null) {
|
||||
newEntry = widget.editEntry!;
|
||||
} else {
|
||||
newEntry = RecurringWalletEntry(
|
||||
id: widget.w.nextId,
|
||||
data: EntryData(amount: 0, name: ""),
|
||||
type: EntryType.expense,
|
||||
date: DateTime.now(),
|
||||
category: widget.w.categories.first,
|
||||
lastRunDate: DateTime.now(),
|
||||
recurType: RecurType.month,
|
||||
);
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(AppLocalizations.of(context).createEntry),
|
||||
),
|
||||
body: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
child: PlatformField(
|
||||
labelText: AppLocalizations.of(context).name,
|
||||
controller: TextEditingController(text: newEntry.data.name),
|
||||
onChanged: (v) {
|
||||
newEntry.data.name = v;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
child: PlatformField(
|
||||
labelText: AppLocalizations.of(context).amount,
|
||||
controller: TextEditingController(
|
||||
text: newEntry.data.amount.toString(),
|
||||
),
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp(r'\d+[\.,]{0,1}\d{0,}'),
|
||||
),
|
||||
],
|
||||
onChanged: (v) {
|
||||
logger.i(v);
|
||||
newEntry.data.amount = double.parse(v);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Text(AppLocalizations.of(context).type),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
child: DropdownButton<EntryType>(
|
||||
value: newEntry.type,
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: EntryType.expense,
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.8 - 24,
|
||||
child: Text(
|
||||
AppLocalizations.of(context).expense,
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: EntryType.income,
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.8 - 24,
|
||||
child: Text(AppLocalizations.of(context).income),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
newEntry.type = v;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Text(AppLocalizations.of(context).category),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
child: DropdownButton<int>(
|
||||
value: newEntry.category.id,
|
||||
items: List.generate(
|
||||
widget.w.categories.length,
|
||||
(index) => DropdownMenuItem(
|
||||
value: widget.w.categories[index].id,
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.8 - 24,
|
||||
child: Text(
|
||||
widget.w.categories[index].name,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
newEntry.category = widget.w.categories
|
||||
.where((element) => element.id == v)
|
||||
.first;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Text(AppLocalizations.of(context).description),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: MediaQuery.of(context).size.width * 0.8,
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||
maxHeight: 300,
|
||||
),
|
||||
child: PlatformField(
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
controller: TextEditingController(
|
||||
text: newEntry.data.description,
|
||||
),
|
||||
onChanged: (v) {
|
||||
newEntry.data.description = v;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)
|
||||
.recurEvery(newEntry.repeatAfter),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
SizedBox(
|
||||
width: 50,
|
||||
child: PlatformField(
|
||||
controller: TextEditingController(
|
||||
text: newEntry.repeatAfter.toString(),
|
||||
),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
FilteringTextInputFormatter.deny(
|
||||
RegExp(r"^0$"),
|
||||
replacementString: "1",
|
||||
),
|
||||
FilteringTextInputFormatter.deny(
|
||||
r"\d+[\.,]{0,1}\d{0,}",
|
||||
replacementString: "1",
|
||||
),
|
||||
],
|
||||
onChanged: (s) {
|
||||
final n = int.tryParse(s);
|
||||
if (n == null) return;
|
||||
newEntry.repeatAfter = n;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: DropdownButton<RecurType>(
|
||||
value: newEntry.recurType,
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: RecurType.day,
|
||||
child: SizedBox(
|
||||
width: 176,
|
||||
child: Text(
|
||||
AppLocalizations.of(context)
|
||||
.dayCounter(newEntry.repeatAfter),
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: RecurType.month,
|
||||
child: SizedBox(
|
||||
width: 176,
|
||||
child: Text(
|
||||
AppLocalizations.of(context)
|
||||
.monthCounter(newEntry.repeatAfter),
|
||||
),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: RecurType.year,
|
||||
child: SizedBox(
|
||||
width: 176,
|
||||
child: Text(
|
||||
AppLocalizations.of(context)
|
||||
.yearCounter(newEntry.repeatAfter),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
newEntry.recurType = v;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(AppLocalizations.of(context).startingWithDate),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
), // TODO: maybe use sizedbox on row with spaceEvenly?
|
||||
PlatformButton(
|
||||
text: DateFormat.yMMMMd(widget.locale)
|
||||
.format(newEntry.lastRunDate),
|
||||
onPressed: () async {
|
||||
final d = await showDatePicker(
|
||||
context: context,
|
||||
firstDate: DateTime.now(),
|
||||
lastDate:
|
||||
DateTime.now().add(const Duration(days: 365)),
|
||||
);
|
||||
if (d == null) return;
|
||||
newEntry.lastRunDate = d;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
PlatformButton(
|
||||
text: AppLocalizations.of(context).save,
|
||||
onPressed: () {
|
||||
if (newEntry.data.name.isEmpty) {
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content:
|
||||
Text(AppLocalizations.of(context).errorEmptyName),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (widget.editEntry != null) {
|
||||
Navigator.of(context).pop(newEntry);
|
||||
return;
|
||||
}
|
||||
widget.w.recurringEntries.add(newEntry);
|
||||
WalletManager.saveWallet(widget.w).then(
|
||||
(value) => Navigator.of(context).pop(widget.w),
|
||||
); // TODO loading circle?
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ 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/walletmanager.dart';
|
||||
import 'package:prasule/api/wallet_manager.dart';
|
||||
import 'package:prasule/main.dart';
|
||||
import 'package:prasule/pw/platformbutton.dart';
|
||||
import 'package:prasule/pw/platformroute.dart';
|
||||
|
|
|
@ -15,9 +15,10 @@ import 'package:intl/date_symbol_data_local.dart';
|
|||
import 'package:intl/intl.dart';
|
||||
import 'package:prasule/api/category.dart';
|
||||
import 'package:prasule/api/entry_data.dart';
|
||||
import 'package:prasule/api/recurring_entry.dart';
|
||||
import 'package:prasule/api/wallet.dart';
|
||||
import 'package:prasule/api/walletentry.dart';
|
||||
import 'package:prasule/api/walletmanager.dart';
|
||||
import 'package:prasule/api/wallet_entry.dart';
|
||||
import 'package:prasule/api/wallet_manager.dart';
|
||||
import 'package:prasule/main.dart';
|
||||
import 'package:prasule/network/tessdata.dart';
|
||||
import 'package:prasule/pw/platformbutton.dart';
|
||||
|
@ -67,6 +68,7 @@ class _HomeViewState extends State<HomeView> {
|
|||
return;
|
||||
}
|
||||
selectedWallet = wallets.first;
|
||||
selectedWallet!.recurEntries();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
|
@ -86,6 +88,7 @@ class _HomeViewState extends State<HomeView> {
|
|||
// debug option to quickly fill a wallet with data
|
||||
if (selectedWallet == null) return;
|
||||
selectedWallet!.entries.clear();
|
||||
selectedWallet!.recurringEntries.clear();
|
||||
final random = Random();
|
||||
for (var i = 0; i < 30; i++) {
|
||||
selectedWallet!.entries.add(
|
||||
|
@ -109,7 +112,41 @@ class _HomeViewState extends State<HomeView> {
|
|||
);
|
||||
}
|
||||
|
||||
logger.i(selectedWallet!.entries.length);
|
||||
logger.d(
|
||||
"Created ${selectedWallet!.entries.length} regular entries",
|
||||
);
|
||||
|
||||
for (var i = 0; i < 3; i++) {
|
||||
final type = random.nextInt(3);
|
||||
selectedWallet!.recurringEntries.add(
|
||||
RecurringWalletEntry(
|
||||
data: EntryData(
|
||||
name: "Recurring Entry #${i + 1}",
|
||||
amount: random.nextInt(20000).toDouble(),
|
||||
),
|
||||
type: (random.nextInt(3) > 0)
|
||||
? EntryType.expense
|
||||
: EntryType.income,
|
||||
date: DateTime(
|
||||
2023,
|
||||
random.nextInt(12) + 1,
|
||||
random.nextInt(28) + 1,
|
||||
),
|
||||
category: selectedWallet!.categories[
|
||||
random.nextInt(selectedWallet!.categories.length)],
|
||||
id: selectedWallet!.nextId,
|
||||
lastRunDate: DateTime.now().subtract(
|
||||
Duration(
|
||||
days: (type > 0) ? 3 : 3 * 31,
|
||||
),
|
||||
),
|
||||
recurType: (type > 0) ? RecurType.day : RecurType.month,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
logger.d(
|
||||
"Created ${selectedWallet!.recurringEntries.length} recurring entries");
|
||||
|
||||
// save and reload
|
||||
WalletManager.saveWallet(selectedWallet!).then((value) {
|
||||
|
@ -127,7 +164,7 @@ class _HomeViewState extends State<HomeView> {
|
|||
onTap: () async {
|
||||
final sw = await Navigator.of(context).push<Wallet>(
|
||||
MaterialPageRoute(
|
||||
builder: (c) => CreateEntryView(w: selectedWallet!),
|
||||
builder: (c) => CreateSingleEntryView(w: selectedWallet!),
|
||||
),
|
||||
);
|
||||
if (sw != null) {
|
||||
|
@ -265,8 +302,8 @@ class _HomeViewState extends State<HomeView> {
|
|||
if (yearA == null) return 0;
|
||||
final yearB = RegExp(r'\d+').firstMatch(b);
|
||||
if (yearB == null) return 0;
|
||||
final compareYears = int.parse(yearA.group(0)!)
|
||||
.compareTo(int.parse(yearB.group(0)!));
|
||||
final compareYears = int.parse(yearB.group(0)!)
|
||||
.compareTo(int.parse(yearA.group(0)!));
|
||||
if (compareYears != 0) return compareYears;
|
||||
final months = List<String>.generate(
|
||||
12,
|
||||
|
@ -291,7 +328,7 @@ class _HomeViewState extends State<HomeView> {
|
|||
Navigator.of(context)
|
||||
.push<WalletSingleEntry>(
|
||||
MaterialPageRoute(
|
||||
builder: (c) => CreateEntryView(
|
||||
builder: (c) => CreateSingleEntryView(
|
||||
w: selectedWallet!,
|
||||
editEntry: element,
|
||||
),
|
||||
|
@ -372,7 +409,9 @@ class _HomeViewState extends State<HomeView> {
|
|||
),
|
||||
title: Text(element.data.name),
|
||||
subtitle: Text(
|
||||
"${element.data.amount} ${selectedWallet!.currency.symbol}",
|
||||
NumberFormat.currency(
|
||||
symbol: selectedWallet!.currency.symbol,
|
||||
).format(element.data.amount),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -472,7 +511,7 @@ class _HomeViewState extends State<HomeView> {
|
|||
final newEntry =
|
||||
await Navigator.of(context).push<WalletSingleEntry>(
|
||||
platformRoute<WalletSingleEntry>(
|
||||
(c) => CreateEntryView(
|
||||
(c) => CreateSingleEntryView(
|
||||
w: selectedWallet!,
|
||||
editEntry: WalletSingleEntry(
|
||||
data: EntryData(
|
||||
|
|
288
lib/views/recurring_view.dart
Normal file
288
lib/views/recurring_view.dart
Normal file
|
@ -0,0 +1,288 @@
|
|||
// ignore_for_file: inference_failure_on_function_invocation
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:prasule/api/recurring_entry.dart';
|
||||
import 'package:prasule/api/wallet.dart';
|
||||
import 'package:prasule/api/wallet_manager.dart';
|
||||
import 'package:prasule/pw/platformbutton.dart';
|
||||
import 'package:prasule/pw/platformdialog.dart';
|
||||
import 'package:prasule/pw/platformroute.dart';
|
||||
import 'package:prasule/util/drawer.dart';
|
||||
import 'package:prasule/util/text_color.dart';
|
||||
import 'package:prasule/views/create_recur_entry.dart';
|
||||
import 'package:prasule/views/settings/settings.dart';
|
||||
import 'package:prasule/views/setup.dart';
|
||||
|
||||
/// Used to set up recurring entries
|
||||
class RecurringEntriesView extends StatefulWidget {
|
||||
/// Used to set up recurring entries
|
||||
const RecurringEntriesView({super.key});
|
||||
|
||||
@override
|
||||
State<RecurringEntriesView> createState() => _RecurringEntriesViewState();
|
||||
}
|
||||
|
||||
class _RecurringEntriesViewState extends State<RecurringEntriesView> {
|
||||
Wallet? selectedWallet;
|
||||
List<Wallet> wallets = [];
|
||||
|
||||
late String locale;
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
locale = Localizations.localeOf(context).languageCode;
|
||||
initializeDateFormatting(Localizations.localeOf(context).languageCode);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
loadWallet();
|
||||
}
|
||||
|
||||
Future<void> loadWallet() async {
|
||||
wallets = await WalletManager.listWallets();
|
||||
if (wallets.isEmpty && mounted) {
|
||||
unawaited(
|
||||
Navigator.of(context)
|
||||
.pushReplacement(platformRoute((c) => const SetupView())),
|
||||
);
|
||||
return;
|
||||
}
|
||||
selectedWallet = wallets.first;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
drawer: makeDrawer(context, 3),
|
||||
appBar: AppBar(
|
||||
title: DropdownButton<int>(
|
||||
value:
|
||||
(selectedWallet == null) ? -1 : wallets.indexOf(selectedWallet!),
|
||||
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 = await WalletManager.listWallets();
|
||||
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 =
|
||||
await WalletManager.loadWallet(selectedWallet!.name);
|
||||
});
|
||||
} else if (value == AppLocalizations.of(context).about) {
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationLegalese: AppLocalizations.of(context).license,
|
||||
applicationName: "Prašule",
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
shape: const CircleBorder(),
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
platformRoute(
|
||||
(p0) => CreateRecurringEntryView(
|
||||
w: selectedWallet!,
|
||||
locale: locale,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
body: Center(
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: (selectedWallet == null)
|
||||
? const Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
],
|
||||
)
|
||||
: (selectedWallet!.recurringEntries.isEmpty)
|
||||
? Column(
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context).noEntries,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
AppLocalizations.of(context).noEntriesSub,
|
||||
),
|
||||
],
|
||||
)
|
||||
: ListView.builder(
|
||||
itemBuilder: (c, i) => Slidable(
|
||||
endActionPane: ActionPane(
|
||||
motion: const ScrollMotion(),
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: (c) {
|
||||
Navigator.of(context)
|
||||
.push<RecurringWalletEntry>(
|
||||
MaterialPageRoute(
|
||||
builder: (c) => CreateRecurringEntryView(
|
||||
w: selectedWallet!,
|
||||
locale: locale,
|
||||
editEntry:
|
||||
selectedWallet!.recurringEntries[i],
|
||||
),
|
||||
),
|
||||
)
|
||||
.then(
|
||||
(editedEntry) {
|
||||
if (editedEntry == null) return;
|
||||
selectedWallet!.entries.remove(
|
||||
selectedWallet!.recurringEntries[i],
|
||||
);
|
||||
selectedWallet!.entries.add(editedEntry);
|
||||
WalletManager.saveWallet(selectedWallet!);
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
},
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSecondary,
|
||||
icon: Icons.edit,
|
||||
),
|
||||
SlidableAction(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.error,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onError,
|
||||
icon: Icons.delete,
|
||||
onPressed: (c) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (cx) => PlatformDialog(
|
||||
title:
|
||||
AppLocalizations.of(context).sureDialog,
|
||||
content: Text(
|
||||
AppLocalizations.of(context).deleteSure,
|
||||
),
|
||||
actions: [
|
||||
PlatformButton(
|
||||
text: AppLocalizations.of(context).yes,
|
||||
onPressed: () {
|
||||
selectedWallet!.recurringEntries
|
||||
.remove(
|
||||
selectedWallet!.recurringEntries[i],
|
||||
);
|
||||
WalletManager.saveWallet(
|
||||
selectedWallet!,
|
||||
);
|
||||
Navigator.of(cx).pop();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
PlatformButton(
|
||||
text: AppLocalizations.of(context).no,
|
||||
onPressed: () {
|
||||
Navigator.of(cx).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: selectedWallet!
|
||||
.recurringEntries[i].category.color,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Icon(
|
||||
selectedWallet!
|
||||
.recurringEntries[i].category.icon,
|
||||
color: selectedWallet!
|
||||
.recurringEntries[i].category.color
|
||||
.calculateTextColor(),
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
selectedWallet!.recurringEntries[i].data.name,
|
||||
),
|
||||
subtitle: Text(
|
||||
NumberFormat.currency(
|
||||
symbol: selectedWallet!.currency.symbol,
|
||||
).format(
|
||||
selectedWallet!.recurringEntries[i].data.amount,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
itemCount: selectedWallet!.recurringEntries.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|||
import 'package:flutter_iconpicker/flutter_iconpicker.dart';
|
||||
import 'package:prasule/api/category.dart';
|
||||
import 'package:prasule/api/wallet.dart';
|
||||
import 'package:prasule/api/walletmanager.dart';
|
||||
import 'package:prasule/api/wallet_manager.dart';
|
||||
import 'package:prasule/main.dart';
|
||||
import 'package:prasule/pw/platformbutton.dart';
|
||||
import 'package:prasule/pw/platformdialog.dart';
|
||||
|
|
|
@ -10,7 +10,7 @@ import 'package:flutter_iconpicker/flutter_iconpicker.dart';
|
|||
import 'package:introduction_screen/introduction_screen.dart';
|
||||
import 'package:prasule/api/category.dart';
|
||||
import 'package:prasule/api/wallet.dart';
|
||||
import 'package:prasule/api/walletmanager.dart';
|
||||
import 'package:prasule/api/wallet_manager.dart';
|
||||
import 'package:prasule/pw/platformbutton.dart';
|
||||
import 'package:prasule/pw/platformdialog.dart';
|
||||
import 'package:prasule/pw/platformfield.dart';
|
||||
|
|
Loading…
Reference in a new issue