Compare commits

..

No commits in common. "42548d437c34dba813f78f6f360e72d7d0132d0e" and "064884dc0793a5e4605b4817ea69820dbdf49e15" have entirely different histories.

14 changed files with 205 additions and 263 deletions

View File

@ -9,7 +9,7 @@ import 'package:sqflite/sqflite.dart';
// Local // Local
import '/models/expense.dart'; import '/models/expense.dart';
import '/models/tracked_item.dart'; import '/models/tracked_type_recurring.dart';
// Leaned on this example: // Leaned on this example:
// https://learnflutterwithme.com/sqlite // https://learnflutterwithme.com/sqlite
@ -77,7 +77,7 @@ class DatabaseHelper {
return expenseList; return expenseList;
} }
Future<int> addExpense(TrackedItem expense) async { Future<int> addExpense(RecurringTrackedType expense) async {
Database db = await instance.db; Database db = await instance.db;
return await db.insert( return await db.insert(
"expense", "expense",
@ -94,7 +94,7 @@ class DatabaseHelper {
); );
} }
Future<int> updateExpense(TrackedItem expense) async { Future<int> updateExpense(RecurringTrackedType expense) async {
Database db = await instance.db; Database db = await instance.db;
return await db.update( return await db.update(
"expense", "expense",

View File

@ -1,7 +1,6 @@
// Local import '/models/tracked_type.dart';
import '/models/tracked_item.dart';
class Asset extends TrackedItem { class Asset extends TrackedType {
static String amountText = "Amount"; static String amountText = "Amount";
Asset({ Asset({

View File

@ -29,7 +29,7 @@ class DatabaseBackup {
'id': e.id, 'id': e.id,
'name': e.name, 'name': e.name,
'cost': e.amount, 'cost': e.amount,
'frequency': e.frequency!.title, 'frequency': e.frequency.title,
'description': e.description, 'description': e.description,
}, },
], ],
@ -39,7 +39,7 @@ class DatabaseBackup {
'id': i.id, 'id': i.id,
'name': i.name, 'name': i.name,
'revenue': i.amount, 'revenue': i.amount,
'frequency': i.frequency!.title, 'frequency': i.frequency.title,
'description': i.description, 'description': i.description,
}, },
], ],

View File

@ -1,15 +1,12 @@
// Local // Local
import 'package:flutter_expense_tracker/models/item_type.dart'; import '/models/tracked_type_recurring.dart';
import '/models/tracked_item.dart';
import '/models/frequency.dart'; import '/models/frequency.dart';
class Expense extends TrackedItem { class Expense extends RecurringTrackedType {
static String amountText = "Cost"; static String amountText = "Cost";
Expense({ Expense({
super.id, super.id,
super.type = ItemType.expense,
required super.name, required super.name,
required super.amount, required super.amount,
required super.frequency, required super.frequency,
@ -32,7 +29,7 @@ class Expense extends TrackedItem {
'id': id, 'id': id,
'name': name, 'name': name,
'cost': amount, 'cost': amount,
'frequency': frequency!.title, 'frequency': frequency.title,
'description': description, 'description': description,
}; };
} }

View File

@ -1,8 +1,8 @@
// Local // Local
import '/models/tracked_item.dart'; import '/models/tracked_type_recurring.dart';
import '/models/frequency.dart'; import '/models/frequency.dart';
class Income extends TrackedItem { class Income extends RecurringTrackedType {
static String amountText = "Revenue"; static String amountText = "Revenue";
Income({ Income({
@ -29,7 +29,7 @@ class Income extends TrackedItem {
'id': id, 'id': id,
'name': name, 'name': name,
'revenue': amount, 'revenue': amount,
'frequency': frequency!.title, 'frequency': frequency.title,
'description': description, 'description': description,
}; };
} }

View File

@ -1,22 +0,0 @@
enum ItemType {
expense(
title: "Expense",
plural: "Expenses",
),
income(
title: "Income",
plural: "Incomes",
),
asset(
title: "Asset",
plural: "Assets",
);
const ItemType({
required this.title,
required this.plural,
});
final String title;
final String plural;
}

View File

@ -1,53 +0,0 @@
// Local
import 'package:flutter_expense_tracker/models/item_type.dart';
import '/models/frequency.dart';
abstract class TrackedItem {
int? id;
ItemType? type;
String name;
double amount;
Frequency? frequency;
String description;
TrackedItem({
this.id,
this.type,
required this.name,
required this.amount,
this.frequency,
required this.description,
});
static String amountText = "Amount";
String getAmountText() => amountText;
@override
String toString() {
return toMap().toString();
}
double calcComparableAmountYearly() {
return frequency == null ? 0 : amount * frequency!.timesPerYear;
}
double calcComparableAmountDaily() {
return frequency == null ? 0 : amount / frequency!.numDays;
}
Map<String, dynamic> toMap() {
return frequency == null ? {
'id': id,
'name': name,
'amount': amount,
'description': description,
} : {
'id': id,
'name': name,
'amount': amount,
'frequency': frequency!.title,
'description': description,
};
}
}

View File

@ -0,0 +1,30 @@
abstract class TrackedType {
int? id;
String name;
double amount;
String description;
TrackedType({
this.id,
required this.name,
required this.amount,
required this.description,
});
static String amountText = "Amount";
String getAmountText() => amountText;
@override
String toString() {
return toMap().toString();
}
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'amount': amount,
'description': description,
};
}
}

View File

@ -0,0 +1,34 @@
// Local
import '/models/tracked_type.dart';
import '/models/frequency.dart';
abstract class RecurringTrackedType extends TrackedType {
Frequency frequency;
RecurringTrackedType({
super.id,
required super.name,
required super.amount,
required this.frequency,
required super.description,
});
double calcComparableAmountYearly() {
return amount * frequency.timesPerYear;
}
double calcComparableAmountDaily() {
return amount / frequency.numDays;
}
@override
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'amount': amount,
'frequency': frequency.title,
'description': description,
};
}
}

12
lib/pages/asset.dart Normal file
View File

@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
class AssetPage extends StatelessWidget {
const AssetPage({
super.key,
});
@override
Widget build(BuildContext context) {
return Placeholder();
}
}

View File

@ -1,30 +1,25 @@
// Flutter // Flutter
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_expense_tracker/models/asset.dart';
import 'package:flutter_expense_tracker/models/income.dart';
// Local // Local
import '/models/tracked_item.dart'; import '/models/tracked_type.dart';
import '/models/item_type.dart'; import '/models/tracked_type_recurring.dart';
import '/models/expense.dart'; import '/models/expense.dart';
import '/models/frequency.dart'; import '/models/frequency.dart';
import '/db.dart'; import '/db.dart';
// TODO: Make this a generic UI based on a superclass of Expense, Income, and Assets. // TODO: Make this a generic UI based on a superclass of Expense, Income, and Assets.
class TrackedItemPage extends StatefulWidget { class ExpensePage extends StatefulWidget {
final Future<List<TrackedItem>> assetsToLoad; const ExpensePage({
const TrackedItemPage({
super.key, super.key,
required this.assetsToLoad,
}); });
@override @override
State<TrackedItemPage> createState() => _TrackedItemPageState(); State<ExpensePage> createState() => _ExpensePageState();
} }
class _TrackedItemPageState extends State<TrackedItemPage> { class _ExpensePageState extends State<ExpensePage> {
refresh() { refresh() {
setState(() {}); setState(() {});
} }
@ -33,10 +28,10 @@ class _TrackedItemPageState extends State<TrackedItemPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return FutureBuilder<List<TrackedItem>>( return FutureBuilder<List<RecurringTrackedType>>(
future: widget.assetsToLoad, future: DatabaseHelper.instance.getExpenses(),
builder: builder: (BuildContext context,
(BuildContext context, AsyncSnapshot<List<TrackedItem>> snapshot) { AsyncSnapshot<List<RecurringTrackedType>> snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return Text('Loading...'); return Text('Loading...');
} }
@ -47,73 +42,65 @@ class _TrackedItemPageState extends State<TrackedItemPage> {
); );
return snapshot.data!.isEmpty return snapshot.data!.isEmpty
? Text( ? Text(
"Add items to get started.", "Add expenses to get started.",
softWrap: true, softWrap: true,
) )
: ListView.builder( : ListView.builder(
itemCount: snapshot.data!.length, itemCount: snapshot.data!.length,
itemBuilder: (_, index) { itemBuilder: (_, index) {
final TrackedItem curr = snapshot.data![index]; //List<Expense> expenses = snapshot.data!;
final RecurringTrackedType curr = snapshot.data![index];
final itemKey = Key(curr.id!.toString()); final itemKey = Key(curr.id!.toString());
final String itemTitle = curr.name; final String itemTitle = curr.name;
final String itemAmount; final String itemAmount;
if (curr.frequency != null) { // TODO: How can we do RecurringTrackedType vs TrackedType here
// if the Widgets are expecting RecurringTrackedType, but we
// need to be using Frequency? Change to only have one abstract
// class and make it nully again? Hmmm...
//if (curr is RecurringTrackedType && curr.frequency != null) {
itemAmount = itemAmount =
"${curr.amount.toStringAsFixed(2)} ${curr.frequency!.title}"; "${curr.amount.toStringAsFixed(2)} ${curr.frequency.title}";
} else { /*} else {
itemAmount = curr.amount.toStringAsFixed(2); itemAmount = curr.amount.toStringAsFixed(2);
} }*/
final String itemDescription = curr.description; final String itemDescription = curr.description;
final double itemDayAmount, itemMonthAmount, itemYearAmount; final double itemDayAmount =
final String estimateSymbolDaily, curr.calcComparableAmountDaily();
estimateSymbolMonthly, final String estimateSymbolDaily = curr.frequency.numDays
estimateSymbolYearly; .toStringAsFixed(2)
.endsWith(".00") &&
if (curr.frequency != null) { itemDayAmount.toStringAsFixed(3).endsWith("0")
itemDayAmount = curr.calcComparableAmountDaily();
estimateSymbolDaily = curr.frequency!.numDays
.toStringAsFixed(2)
.endsWith(".00") &&
itemDayAmount.toStringAsFixed(3).endsWith("0")
? ""
: "~";
itemMonthAmount =
(curr.calcComparableAmountYearly() / 12);
estimateSymbolMonthly = curr.frequency!.timesPerYear
.toStringAsFixed(2)
.endsWith(".00") &&
itemMonthAmount.toStringAsFixed(3).endsWith("0")
? ""
: "~";
itemYearAmount = curr.calcComparableAmountYearly();
estimateSymbolYearly = curr.frequency!.timesPerYear
.toStringAsFixed(2)
.endsWith(".00") &&
itemYearAmount.toStringAsFixed(3).endsWith("0")
? ""
: "~";
} else {
itemDayAmount = -1;
estimateSymbolDaily = "";
itemMonthAmount = curr.amount;
estimateSymbolMonthly = "";
itemYearAmount = -1;
estimateSymbolYearly = "";
}
final String itemTopText = itemDayAmount < 0
? "" ? ""
: "$estimateSymbolDaily${itemDayAmount.toStringAsFixed(2)} ${Frequency.daily.title}"; : "~";
final String itemMiddleText = itemMonthAmount < 0
final double itemMonthAmount =
(curr.calcComparableAmountYearly() / 12);
final String estimateSymbolMonthly = curr
.frequency.timesPerYear
.toStringAsFixed(2)
.endsWith(".00") &&
itemMonthAmount.toStringAsFixed(3).endsWith("0")
? "" ? ""
: "$estimateSymbolMonthly${itemMonthAmount.toStringAsFixed(2)} ${Frequency.monthly.title}"; : "~";
final String itemBottomText = itemYearAmount < 0
final double itemYearAmount =
curr.calcComparableAmountYearly();
final String estimateSymbolYearly = curr
.frequency.timesPerYear
.toStringAsFixed(2)
.endsWith(".00") &&
itemYearAmount.toStringAsFixed(3).endsWith("0")
? "" ? ""
: "$estimateSymbolYearly${itemYearAmount.toStringAsFixed(2)} ${Frequency.yearly.title}"; : "~";
final String itemTopText =
"$estimateSymbolDaily${itemDayAmount.toStringAsFixed(2)} ${Frequency.daily.title}";
final String itemMiddleText =
"$estimateSymbolMonthly${itemMonthAmount.toStringAsFixed(2)} ${Frequency.monthly.title}";
final String itemBottomText =
"$estimateSymbolYearly${itemYearAmount.toStringAsFixed(2)} ${Frequency.yearly.title}";
return Padding( return Padding(
padding: const EdgeInsets.all(4.0), padding: const EdgeInsets.all(4.0),
@ -144,28 +131,17 @@ class _TrackedItemPageState extends State<TrackedItemPage> {
snapshot.data!.remove(curr); snapshot.data!.remove(curr);
switch (direction) { switch (direction) {
case DismissDirection.startToEnd: case DismissDirection.startToEnd:
if (curr is Expense) { DatabaseHelper.instance.removeExpense(curr.id!);
DatabaseHelper.instance
.removeExpense(curr.id!);
} else if (curr is Income) {
// TODO
} else if (curr is Asset) {
// TODO
} else {
throw UnimplementedError(
"Cannot remove unimplemented item type.");
}
break; break;
case DismissDirection.endToStart: case DismissDirection.endToStart:
// Open an edit dialog, then remove the item from the list. // Open an edit dialog, then remove the item from the list.
showDialog( showDialog(
context: context, context: context,
builder: (_) => AlertDialog( builder: (_) => AlertDialog(
content: TrackedItemInputDialog( content: RecurringTrackedTypeInputDialog(
notifyParent: refresh, notifyParent: refresh,
entry: curr, entry: curr,
amountText: curr.getAmountText(), amountText: curr.getAmountText(),
type: curr.type!,
), ),
), ),
); );
@ -242,26 +218,26 @@ class _TrackedItemPageState extends State<TrackedItemPage> {
} }
} }
class TrackedItemInputDialog extends StatefulWidget { class RecurringTrackedTypeInputDialog extends StatefulWidget {
final Function() notifyParent; final Function() notifyParent;
final TrackedItem? entry; final RecurringTrackedType? entry;
final String? amountText; final String? amountText;
final ItemType? type;
const TrackedItemInputDialog({ const RecurringTrackedTypeInputDialog({
super.key, super.key,
required this.notifyParent, required this.notifyParent,
this.entry, this.entry,
this.amountText, this.amountText,
this.type,
}); });
@override @override
State<TrackedItemInputDialog> createState() => _TrackedItemInputDialogState(); State<RecurringTrackedTypeInputDialog> createState() =>
_RecurringTrackedTypeInputDialogState();
} }
class _TrackedItemInputDialogState extends State<TrackedItemInputDialog> { class _RecurringTrackedTypeInputDialogState
final _formKey = GlobalKey<FormState>(); extends State<RecurringTrackedTypeInputDialog> {
final _expenseFormKey = GlobalKey<FormState>();
int? _id; int? _id;
String _name = ""; String _name = "";
@ -271,23 +247,16 @@ class _TrackedItemInputDialogState extends State<TrackedItemInputDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.type == null &&
(widget.entry != null && widget.entry!.type == null)) {
throw FlutterError("No ItemType provided for TrackedItemInputDialog.");
}
ItemType? _type = widget.type;
if (widget.entry != null) { if (widget.entry != null) {
_id = widget.entry!.id; _id = widget.entry!.id;
_name = widget.entry!.name; _name = widget.entry!.name;
_amount = widget.entry!.amount; _amount = widget.entry!.amount;
widget.entry!.frequency == null ? null : _freq = widget.entry!.frequency!; _freq = widget.entry!.frequency;
_desc = widget.entry!.description; _desc = widget.entry!.description;
_type = widget.entry!.type!;
} }
String amountText = String amountText =
widget.amountText != null ? widget.amountText! : TrackedItem.amountText; widget.amountText != null ? widget.amountText! : TrackedType.amountText;
return Column( return Column(
// prevent AlertDialog from taking full vertical height. // prevent AlertDialog from taking full vertical height.
@ -299,20 +268,7 @@ class _TrackedItemInputDialogState extends State<TrackedItemInputDialog> {
onPressed: () { onPressed: () {
if (widget.entry != null) { if (widget.entry != null) {
setState(() { setState(() {
switch (_type) { DatabaseHelper.instance.addExpense(widget.entry!);
case ItemType.expense:
DatabaseHelper.instance.addExpense(widget.entry!);
break;
case ItemType.income:
// TODO
break;
case ItemType.asset:
// TODO
break;
default:
throw UnimplementedError(
"Cannot add unimplemented type.");
}
widget.notifyParent(); widget.notifyParent();
}); });
} }
@ -325,7 +281,7 @@ class _TrackedItemInputDialogState extends State<TrackedItemInputDialog> {
insetPadding: EdgeInsets.all(0), insetPadding: EdgeInsets.all(0),
title: Center( title: Center(
child: widget.entry == null child: widget.entry == null
? Text("New ${_type!.title}") ? Text("New Expense")
: Text("Edit Expense"), : Text("Edit Expense"),
), ),
content: FutureBuilder<List<Expense>>( content: FutureBuilder<List<Expense>>(
@ -337,7 +293,7 @@ class _TrackedItemInputDialogState extends State<TrackedItemInputDialog> {
} }
List<Expense> expenses = snapshot.data!; List<Expense> expenses = snapshot.data!;
return Form( return Form(
key: _formKey, key: _expenseFormKey,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -460,8 +416,8 @@ class _TrackedItemInputDialogState extends State<TrackedItemInputDialog> {
Center( Center(
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () { onPressed: () {
if (_formKey.currentState!.validate()) { if (_expenseFormKey.currentState!.validate()) {
_formKey.currentState!.save(); _expenseFormKey.currentState!.save();
setState(() { setState(() {
Expense expense = Expense( Expense expense = Expense(
id: _id, id: _id,

View File

@ -1,14 +1,14 @@
// Flutter // Flutter
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_expense_tracker/models/item_type.dart';
import 'dart:io'; import 'dart:io';
// Local // Local
import '/pages/tracked_item.dart'; import '/pages/expense.dart';
import '/pages/income.dart';
import '/pages/asset.dart';
import '/pages/report.dart'; import '/pages/report.dart';
import '/pages/settings.dart'; import '/pages/settings.dart';
import '/pages/help.dart'; import '/pages/help.dart';
import '/db.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
const HomePage({ const HomePage({
@ -32,15 +32,14 @@ class _HomePageState extends State<HomePage> {
Widget? dialog; Widget? dialog;
switch (pageSelected) { switch (pageSelected) {
case 0: case 0:
page = TrackedItemPage(assetsToLoad: DatabaseHelper.instance.getExpenses()); page = ExpensePage();
dialog = TrackedItemInputDialog( dialog = RecurringTrackedTypeInputDialog(
notifyParent: refresh, notifyParent: refresh,
type: ItemType.expense,
); );
case 1: case 1:
page = Placeholder(); page = IncomePage();
case 2: case 2:
page = Placeholder(); page = AssetPage();
case 3: case 3:
page = ProjectionPage(); page = ProjectionPage();
case 4: case 4:

12
lib/pages/income.dart Normal file
View File

@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
class IncomePage extends StatelessWidget {
const IncomePage({
super.key,
});
@override
Widget build(BuildContext context) {
return Placeholder();
}
}

View File

@ -2,8 +2,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
// Local // Local
import '/db.dart'; import 'package:flutter_expense_tracker/db.dart';
import '/models/tracked_item.dart'; import 'package:flutter_expense_tracker/models/tracked_type_recurring.dart';
/// TODO: /// TODO:
/// - Expenses (total number, totals by day / month / year) /// - Expenses (total number, totals by day / month / year)
@ -57,39 +57,31 @@ class SummaryCardForTotals extends StatelessWidget {
required this.summaryTypeLabel, required this.summaryTypeLabel,
}); });
final Future<List<TrackedItem>> list; final Future<List<RecurringTrackedType>> list;
final String summaryTypeLabel; final String summaryTypeLabel;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder<List<TrackedItem>>( return FutureBuilder<List<RecurringTrackedType>>(
future: list, future: list,
builder: ( builder: (
BuildContext context, BuildContext context,
AsyncSnapshot<List<TrackedItem>> snapshot, AsyncSnapshot<List<RecurringTrackedType>> snapshot,
) { ) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return Text('Loading $summaryTypeLabel Section...'); return Text('Loading $summaryTypeLabel Section...');
} }
double dailyTotal = 0, monthlyTotal = 0, yearlyTotal = 0; double dailyTotal = 0, monthlyTotal = 0, yearlyTotal = 0;
for (TrackedItem e in snapshot.data!) { for (RecurringTrackedType e in snapshot.data!) {
dailyTotal += e.calcComparableAmountDaily(); dailyTotal += e.calcComparableAmountDaily();
monthlyTotal += e.calcComparableAmountYearly() / 12; monthlyTotal += e.calcComparableAmountYearly() / 12;
yearlyTotal += e.calcComparableAmountYearly(); yearlyTotal += e.calcComparableAmountYearly();
} }
String dailyEstimate =
dailyTotal.toStringAsFixed(3).endsWith("0") ? "" : "~",
monthlyEstimate =
monthlyTotal.toStringAsFixed(3).endsWith("0") ? "" : "~",
yearlyEstimate =
yearlyTotal.toStringAsFixed(3).endsWith("0") ? "" : "~";
return SummaryCard( return SummaryCard(
name: "$summaryTypeLabel Totals", name: "$summaryTypeLabel Totals",
leftText: "$dailyEstimate${dailyTotal.toStringAsFixed(2)} Daily", leftText: "${dailyTotal.toStringAsFixed(2)} Daily",
middleText: middleText: "${monthlyTotal.toStringAsFixed(2)} Monthly",
"$monthlyEstimate${monthlyTotal.toStringAsFixed(2)} Monthly", rightText: "${yearlyTotal.toStringAsFixed(2)} Yearly",
rightText:
"$yearlyEstimate${yearlyTotal.toStringAsFixed(2)} Yearly",
); );
}); });
} }
@ -112,41 +104,27 @@ class SummaryCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
color: Theme.of(context).cardColor,
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Card( child: Column(
color: Theme.of(context).highlightColor, children: [
child: Column( Row(
children: [ children: [
Text( Text(leftText),
name, Spacer(),
style: TextStyle( Center(
decoration: TextDecoration.underline, child: Column(
fontSize: 16 children: [
Text(name),
Text(middleText),
],
),
), ),
), Spacer(),
Row( Text(rightText),
children: [ ],
Spacer( ),
flex: 3, ],
),
Text(leftText),
Spacer(
flex: 1,
),
Text(middleText),
Spacer(
flex: 1,
),
Text(rightText),
Spacer(
flex: 3,
),
],
),
],
),
), ),
), ),
); );