Huge improvements to UI, very interactive now!
This commit is contained in:
parent
15fa4aadbd
commit
2acabf4d3b
@ -7,32 +7,108 @@ import '/models/frequency.dart';
|
||||
|
||||
List<Expense> expenses = [];
|
||||
|
||||
class ExpensePage extends StatelessWidget {
|
||||
class ExpensePage extends StatefulWidget {
|
||||
const ExpensePage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ExpensePage> createState() => _ExpensePageState();
|
||||
}
|
||||
|
||||
class _ExpensePageState extends State<ExpensePage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
expenses.sort(
|
||||
(a, b) => (b.calcComparableCost() - a.calcComparableCost()).toInt(),
|
||||
);
|
||||
return expenses.isEmpty
|
||||
? Text("Add expenses to get started!")
|
||||
: ListView.builder(
|
||||
itemCount: expenses.length,
|
||||
itemBuilder: (_, index) {
|
||||
final Expense curr = expenses[index];
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
final String estimateSymbol = switch (curr.frequency.timesPerYear
|
||||
.toStringAsFixed(2)
|
||||
.endsWith(".00")) {
|
||||
true => "",
|
||||
false => "~",
|
||||
};
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Dismissible(
|
||||
key: Key(curr.toString()),
|
||||
background: Container(
|
||||
color: Colors.red,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.delete),
|
||||
Text("Delete!"),
|
||||
Spacer(),
|
||||
Text("Delete!"),
|
||||
Icon(Icons.delete),
|
||||
],
|
||||
),
|
||||
),
|
||||
onDismissed: (direction) {
|
||||
setState(() {
|
||||
expenses.remove(curr);
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: Colors.greenAccent,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(curr.name),
|
||||
Text("${curr.cost.toString()} ${curr.frequency.title}"),
|
||||
],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
curr.name,
|
||||
style: TextStyle(fontSize: 20.0),
|
||||
),
|
||||
Text(
|
||||
"${curr.cost.toStringAsFixed(2)} ${curr.frequency.title}",
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
),
|
||||
if (curr.frequency != Frequency.yearly)
|
||||
Text(
|
||||
"$estimateSymbol${curr.calcComparableCost().toStringAsFixed(2)} Yearly",
|
||||
style: TextStyle(fontSize: 12.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
curr.description,
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
),
|
||||
softWrap: true,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.edit_off),
|
||||
onPressed: () {
|
||||
// TODO: Open the item in the dialog with the NAME field disabled.
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Editing still TBD"),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -63,118 +139,158 @@ class _ExpenseInputDialogState extends State<ExpenseInputDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Center(
|
||||
child: Text("New Expense"),
|
||||
),
|
||||
content: Form(
|
||||
key: _expenseFormKey,
|
||||
//autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
//spacing: 10,
|
||||
children: [
|
||||
TextFormField(
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Name",
|
||||
hintText: "Example: Red Pocket Phone Bill",
|
||||
return Column(
|
||||
// prevent AlertDialog from taking full vertical height.
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
alignment: FractionalOffset.topRight,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
icon: Icon(Icons.clear),
|
||||
),
|
||||
),
|
||||
AlertDialog(
|
||||
insetPadding: EdgeInsets.all(0),
|
||||
title: Text("New Expense"),
|
||||
content: Form(
|
||||
key: _expenseFormKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Name",
|
||||
hintText: "Example: Red Pocket",
|
||||
hintStyle: TextStyle(fontSize: 12.0),
|
||||
errorStyle: TextStyle(fontSize: 10.0),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty) {
|
||||
return "Name must be provided.";
|
||||
}
|
||||
if (!expenses.every((expense) => expense.name != value)) {
|
||||
return "Name must be unique, already in use.";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
_name = value!;
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: InputDecoration(
|
||||
labelText: "Cost",
|
||||
hintText: "Example: 10.00",
|
||||
hintStyle: TextStyle(fontSize: 12.0),
|
||||
errorStyle: TextStyle(fontSize: 10.0),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Cost 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.";
|
||||
}
|
||||
if (double.tryParse(value) == null) {
|
||||
return "Cost must be a valid number.";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
_cost = double.parse(value!);
|
||||
},
|
||||
),
|
||||
DropdownButtonFormField(
|
||||
items: Frequency.values
|
||||
.map(
|
||||
(freq) => DropdownMenuItem(
|
||||
value: freq,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
freq.title,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(1.0),
|
||||
child: Text(
|
||||
" (${freq.hint})",
|
||||
style: TextStyle(fontSize: 10.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
value: Frequency.montly,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Frequency",
|
||||
errorStyle: TextStyle(fontSize: 10.0),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null) {
|
||||
return "Frequency must be provided.";
|
||||
}
|
||||
if (!Frequency.values.contains(value)) {
|
||||
return "Value not valid.";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) {
|
||||
_freq = value!;
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Description",
|
||||
hintText: "Example: 1GB data with unlimited talk & text.",
|
||||
hintStyle: TextStyle(fontSize: 12.0),
|
||||
errorStyle: TextStyle(fontSize: 10.0),
|
||||
),
|
||||
validator: (value) {
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
_desc = value!;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Center(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
if (_expenseFormKey.currentState!.validate()) {
|
||||
_expenseFormKey.currentState!.save();
|
||||
setState(() {
|
||||
expenses.add(
|
||||
Expense(
|
||||
name: _name,
|
||||
cost: _cost,
|
||||
frequency: _freq,
|
||||
description: _desc),
|
||||
);
|
||||
});
|
||||
widget.notifyParent();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
icon: Icon(Icons.save),
|
||||
label: Text('Submit'),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty) {
|
||||
return "Name must be provided.";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (newValue) {
|
||||
_name = newValue!;
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: InputDecoration(
|
||||
labelText: "Cost", hintText: "Example: 10.00"),
|
||||
validator: (value) {
|
||||
if (value!.isEmpty) {
|
||||
return "Cost must be provided.";
|
||||
}
|
||||
if (double.tryParse(value) == null) {
|
||||
return "Cost must be a valid number.";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (newValue) {
|
||||
_cost = double.parse(newValue!);
|
||||
},
|
||||
),
|
||||
DropdownButtonFormField(
|
||||
items: (Frequency.values.map((freq) =>
|
||||
DropdownMenuItem(value: freq, child: Text(freq.title))))
|
||||
.toList(),
|
||||
decoration: InputDecoration(
|
||||
labelText: "Recurrence", hintText: "Example: Monthly"),
|
||||
validator: (value) {
|
||||
if (value == null) {
|
||||
return "Frequency must be provided.";
|
||||
}
|
||||
if (!Frequency.values.contains(value)) {
|
||||
return "Value not valid.";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (newValue) {
|
||||
_freq = newValue!;
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Description",
|
||||
hintText: "Example: 1GB data with unlimited talk & text."),
|
||||
validator: (value) {
|
||||
return null;
|
||||
},
|
||||
onSaved: (newValue) {
|
||||
_desc = newValue!;
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
icon: Icon(Icons.cancel),
|
||||
label: Text('Cancel'),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
if (_expenseFormKey.currentState!.validate()) {
|
||||
_expenseFormKey.currentState!.save();
|
||||
setState(() {
|
||||
expenses.add(
|
||||
Expense(
|
||||
name: _name,
|
||||
cost: _cost,
|
||||
frequency: _freq,
|
||||
description: _desc),
|
||||
);
|
||||
});
|
||||
widget.notifyParent();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
icon: Icon(Icons.save),
|
||||
label: Text('Submit'),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user