Begin making the Expense page more arbitrary for future re-use by creating superclasses which will fit all 3 object use cases.

This commit is contained in:
Hyperling 2025-02-11 14:33:38 -07:00
parent 7a3eaf70b5
commit ab9b3e0bf9
4 changed files with 91 additions and 55 deletions

View File

@ -1,51 +1,24 @@
import '/models/recurring_tracked_type.dart';
import '/models/frequency.dart';
class Expense {
final int? id;
final String name;
final double cost;
final Frequency frequency;
final String description;
class Expense extends RecurringTrackedType {
static String amountText = "Cost";
const Expense({
this.id,
required this.name,
required this.cost,
required this.frequency,
required this.description,
Expense({
super.id,
required super.name,
required super.amount,
required super.frequency,
required super.description,
});
@override
String toString() {
//return "$name, $cost, ${frequency.title}, $description";
return toMap().toString();
}
double calcComparableCost() {
return cost * frequency.timesPerYear;
}
double calcComparableCostDaily() {
return cost / frequency.numDays;
}
factory Expense.fromMap(Map<String, dynamic> json) => Expense(
id: json['id'],
name: json['name'],
cost: json['cost'],
amount: json['cost'],
frequency: Frequency.values
.where((expense) => expense.title == json['frequency'])
.where((freq) => freq.title == json['frequency'])
.first,
description: json['description'],
);
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'cost': cost,
'frequency': frequency.title,
'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,
};
}
}

View File

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

View File

@ -6,7 +6,7 @@ import 'package:flutter_expense_tracker/db.dart';
import '/models/expense.dart';
import '/models/frequency.dart';
// TODO: Make this a generic class based on a suerclass of Expense, Income, and Assets?
// TODO: Make this a generic class based on a superclass of Expense, Income, and Assets?
class ExpensePage extends StatefulWidget {
const ExpensePage({
@ -30,10 +30,12 @@ class _ExpensePageState extends State<ExpensePage> {
future: DatabaseHelper.instance.getExpenses(),
builder: (BuildContext context, AsyncSnapshot<List<Expense>> snapshot) {
if (!snapshot.hasData) {
return Center(child: Text('Loading...'));
return Text('Loading...');
}
snapshot.data!.sort(
(a, b) => (b.calcComparableCost() - a.calcComparableCost()).toInt(),
(a, b) => (b.calcComparableAmountYearly() -
a.calcComparableAmountYearly())
.toInt(),
);
return snapshot.data!.isEmpty
? Text(
@ -50,7 +52,7 @@ class _ExpensePageState extends State<ExpensePage> {
.toStringAsFixed(2)
.endsWith(".00") &&
curr
.calcComparableCost()
.calcComparableAmountYearly()
.toStringAsFixed(3)
.endsWith("0")
? ""
@ -59,7 +61,7 @@ class _ExpensePageState extends State<ExpensePage> {
.toStringAsFixed(2)
.endsWith(".00") &&
curr
.calcComparableCostDaily()
.calcComparableAmountDaily()
.toStringAsFixed(3)
.endsWith("0")
? ""
@ -133,7 +135,7 @@ class _ExpensePageState extends State<ExpensePage> {
style: TextStyle(fontSize: 20.0),
),
Text(
"${curr.cost.toStringAsFixed(2)} ${curr.frequency.title}",
"${curr.amount.toStringAsFixed(2)} ${curr.frequency.title}",
style: TextStyle(fontSize: 12.0),
),
],
@ -155,12 +157,12 @@ class _ExpensePageState extends State<ExpensePage> {
children: [
//if (curr.frequency != Frequency.daily)
Text(
"$estimateSymbolDaily${curr.calcComparableCostDaily().toStringAsFixed(2)} ${Frequency.daily.title}",
"$estimateSymbolDaily${curr.calcComparableAmountDaily().toStringAsFixed(2)} ${Frequency.daily.title}",
style: TextStyle(fontSize: 12.0),
),
//if (curr.frequency != Frequency.yearly)
Text(
"$estimateSymbolYearly${curr.calcComparableCost().toStringAsFixed(2)} ${Frequency.yearly.title}",
"$estimateSymbolYearly${curr.calcComparableAmountYearly().toStringAsFixed(2)} ${Frequency.yearly.title}",
style: TextStyle(fontSize: 12.0),
),
],
@ -196,7 +198,7 @@ class _ExpenseInputDialogState extends State<ExpenseInputDialog> {
int? _id;
String _name = "";
double _cost = 0;
double _amount = 0;
Frequency _freq = Frequency.monthly;
String _desc = "";
@ -205,7 +207,7 @@ class _ExpenseInputDialogState extends State<ExpenseInputDialog> {
if (widget.expense != null) {
_id = widget.expense!.id;
_name = widget.expense!.name;
_cost = widget.expense!.cost;
_amount = widget.expense!.amount;
_freq = widget.expense!.frequency;
_desc = widget.expense!.description;
}
@ -277,29 +279,29 @@ class _ExpenseInputDialogState extends State<ExpenseInputDialog> {
keyboardType:
TextInputType.numberWithOptions(decimal: true),
decoration: InputDecoration(
labelText: "Cost",
labelText: "${Expense.amountText}",
hintText: "Example: 10.00",
hintStyle: TextStyle(fontSize: 10.0),
errorStyle: TextStyle(fontSize: 10.0),
),
initialValue: _cost != 0 ? _cost.toString() : "",
initialValue: _amount != 0 ? _amount.toString() : "",
validator: (value) {
if (value == null || value.isEmpty) {
return "Cost must be provided.";
return "${Expense.amountText} must be provided.";
}
if (double.parse(value) < 0) {
return "Please use the Income page rather than having negative expenses.";
}
if (double.parse(value) < 0.01) {
return "Cost must be one hundreth (0.01) or higher.";
return "${Expense.amountText} must be one hundreth (0.01) or higher.";
}
if (double.tryParse(value) == null) {
return "Cost must be a valid number.";
return "${Expense.amountText} must be a valid number.";
}
return null;
},
onSaved: (value) {
_cost = double.parse(value!);
_amount = double.parse(value!);
},
),
DropdownButtonFormField(
@ -374,7 +376,7 @@ class _ExpenseInputDialogState extends State<ExpenseInputDialog> {
Expense expense = Expense(
id: _id,
name: _name,
cost: _cost,
amount: _amount,
frequency: _freq,
description: _desc,
);