Compare commits
10 Commits
last_versi
...
9d8a5e6685
Author | SHA1 | Date | |
---|---|---|---|
9d8a5e6685 | |||
2acabf4d3b | |||
15fa4aadbd | |||
78a407d0ec | |||
54cd86c34b | |||
66fd966de8 | |||
5561f50736 | |||
6b25e6e552 | |||
360a36f024 | |||
ecbac615e9 |
45
lib/db.dart
Normal file
45
lib/db.dart
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// https://docs.flutter.dev/cookbook/persistence/sqlite
|
||||||
|
|
||||||
|
// SQLite
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_expense_tracker/models/frequency.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
|
||||||
|
// Local
|
||||||
|
import '/models/expense.dart';
|
||||||
|
|
||||||
|
void loadDB() async {
|
||||||
|
// Avoid errors caused by flutter upgrade.
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
final String frequencies =
|
||||||
|
"'${Frequency.values.map((freq) => freq.title).join("','")}'";
|
||||||
|
print(frequencies);
|
||||||
|
|
||||||
|
// Open the database and store the reference.
|
||||||
|
final database = openDatabase(
|
||||||
|
// Set the path to the database. Note: Using the `join` function from the
|
||||||
|
// `path` package is best practice to ensure the path is correctly
|
||||||
|
// constructed for each platform.
|
||||||
|
join(await getDatabasesPath(), 'expense_tracker.db'),
|
||||||
|
|
||||||
|
onCreate: (db, version) {
|
||||||
|
// Run the CREATE TABLE statement on the database.
|
||||||
|
return db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE expense
|
||||||
|
( id INTEGER PRIMARY KEY
|
||||||
|
, name TEXT
|
||||||
|
, cost DOUBLE
|
||||||
|
, frequency TEXT CHECK(frequency IN ($frequencies) )
|
||||||
|
, description TEXT
|
||||||
|
)""",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
// Set the version. This executes the onCreate function and provides a
|
||||||
|
// path to perform database upgrades and downgrades.
|
||||||
|
version: 1,
|
||||||
|
);
|
||||||
|
}
|
@ -1,15 +1,16 @@
|
|||||||
// Helpful guides:
|
// Flutter
|
||||||
// - https://flutter.dev/docs/cookbook/forms/validation
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
// Local
|
||||||
import '/pages/expense.dart';
|
import '/pages/expense.dart';
|
||||||
import '/pages/income.dart';
|
import '/pages/income.dart';
|
||||||
import '/pages/asset.dart';
|
import '/pages/asset.dart';
|
||||||
import '/pages/report.dart';
|
import '/pages/report.dart';
|
||||||
import '/pages/settings.dart';
|
import '/pages/settings.dart';
|
||||||
|
import '/db.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
loadDB();
|
||||||
runApp(const MainApp());
|
runApp(const MainApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,6 +42,10 @@ class HomePage extends StatefulWidget {
|
|||||||
class _HomePageState extends State<HomePage> {
|
class _HomePageState extends State<HomePage> {
|
||||||
var pageSelected = 0;
|
var pageSelected = 0;
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget page;
|
Widget page;
|
||||||
@ -48,7 +53,9 @@ class _HomePageState extends State<HomePage> {
|
|||||||
switch (pageSelected) {
|
switch (pageSelected) {
|
||||||
case 0:
|
case 0:
|
||||||
page = ExpensePage();
|
page = ExpensePage();
|
||||||
dialog = ExpenseInputDialog();
|
dialog = ExpenseInputDialog(
|
||||||
|
notifyParent: refresh,
|
||||||
|
);
|
||||||
case 1:
|
case 1:
|
||||||
page = IncomePage();
|
page = IncomePage();
|
||||||
case 2:
|
case 2:
|
||||||
@ -79,49 +86,42 @@ class _HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
return LayoutBuilder(builder: (context, constraints) {
|
return LayoutBuilder(builder: (context, constraints) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
// TODO: Add a drawer instead of nav rail.
|
appBar: AppBar(title: Text("Expense Tracker")),
|
||||||
body: Row(
|
drawer: NavigationRail(
|
||||||
children: [
|
extended: true,
|
||||||
SafeArea(
|
destinations: [
|
||||||
child: NavigationRail(
|
NavigationRailDestination(
|
||||||
extended: constraints.maxWidth >= 800,
|
icon: Icon(Icons.payment),
|
||||||
destinations: [
|
label: Text('Expenses'),
|
||||||
NavigationRailDestination(
|
|
||||||
icon: Icon(Icons.payment),
|
|
||||||
label: Text('Expenses'),
|
|
||||||
),
|
|
||||||
NavigationRailDestination(
|
|
||||||
icon: Icon(Icons.account_balance),
|
|
||||||
label: Text('Income'),
|
|
||||||
),
|
|
||||||
NavigationRailDestination(
|
|
||||||
icon: Icon(Icons.attach_money),
|
|
||||||
label: Text('Liquid Assets'),
|
|
||||||
),
|
|
||||||
NavigationRailDestination(
|
|
||||||
icon: Icon(Icons.bar_chart),
|
|
||||||
label: Text('Projections'),
|
|
||||||
),
|
|
||||||
NavigationRailDestination(
|
|
||||||
icon: Icon(Icons.settings),
|
|
||||||
label: Text('Settings'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
selectedIndex: pageSelected,
|
|
||||||
onDestinationSelected: (value) {
|
|
||||||
setState(() {
|
|
||||||
pageSelected = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Expanded(
|
NavigationRailDestination(
|
||||||
child: Container(
|
icon: Icon(Icons.account_balance),
|
||||||
color: Theme.of(context).colorScheme.primaryContainer,
|
label: Text('Income'),
|
||||||
child: page,
|
),
|
||||||
),
|
NavigationRailDestination(
|
||||||
|
icon: Icon(Icons.attach_money),
|
||||||
|
label: Text('Liquid Assets'),
|
||||||
|
),
|
||||||
|
NavigationRailDestination(
|
||||||
|
icon: Icon(Icons.bar_chart),
|
||||||
|
label: Text('Reports'),
|
||||||
|
),
|
||||||
|
NavigationRailDestination(
|
||||||
|
icon: Icon(Icons.settings),
|
||||||
|
label: Text('Settings'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
selectedIndex: pageSelected,
|
||||||
|
onDestinationSelected: (value) {
|
||||||
|
setState(() {
|
||||||
|
pageSelected = value;
|
||||||
|
Navigator.pop(context);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
child: Center(child: page),
|
||||||
),
|
),
|
||||||
floatingActionButton: floatingButton,
|
floatingActionButton: floatingButton,
|
||||||
);
|
);
|
||||||
|
@ -15,4 +15,8 @@ class Expense {
|
|||||||
String toString() {
|
String toString() {
|
||||||
return "$name, $cost, ${frequency.title}, $description";
|
return "$name, $cost, ${frequency.title}, $description";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double calcComparableCost() {
|
||||||
|
return cost * frequency.timesPerYear;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,53 @@
|
|||||||
// https://www.tutorialspoint.com/dart_programming/dart_programming_enumeration.htm
|
// https://www.tutorialspoint.com/dart_programming/dart_programming_enumeration.htm
|
||||||
enum Frequency {
|
enum Frequency {
|
||||||
daily(title: "Daily"),
|
daily(
|
||||||
weekly(title: "Weekly"),
|
title: "Daily",
|
||||||
biweekly(title: "Biweekly"),
|
hint: "Once Per Day",
|
||||||
montly(title: "Monthly"),
|
timesPerYear: 364.25,
|
||||||
yearly(title: "Yearly");
|
),
|
||||||
|
weekly(
|
||||||
|
title: "Weekly",
|
||||||
|
hint: "Once Per Week",
|
||||||
|
timesPerYear: (364.25/7),
|
||||||
|
),
|
||||||
|
biweekly(
|
||||||
|
title: "Biweekly",
|
||||||
|
hint: "Every Other Week",
|
||||||
|
timesPerYear: (364.25/7/2),
|
||||||
|
),
|
||||||
|
bimonthly(
|
||||||
|
title: "Bimonthly",
|
||||||
|
hint: "Twice Per Month",
|
||||||
|
timesPerYear: 24,
|
||||||
|
),
|
||||||
|
montly(
|
||||||
|
title: "Monthly",
|
||||||
|
hint: "Once Per Month",
|
||||||
|
timesPerYear: 12,
|
||||||
|
),
|
||||||
|
quarterly(
|
||||||
|
title: "Quarterly",
|
||||||
|
hint: "Every Three Months",
|
||||||
|
timesPerYear: 4,
|
||||||
|
),
|
||||||
|
biannual(
|
||||||
|
title: "Biannual",
|
||||||
|
hint: "Twice Per Year",
|
||||||
|
timesPerYear: 2,
|
||||||
|
),
|
||||||
|
yearly(
|
||||||
|
title: "Yearly",
|
||||||
|
hint: "Once Per Year",
|
||||||
|
timesPerYear: 1,
|
||||||
|
);
|
||||||
|
|
||||||
const Frequency({required this.title});
|
const Frequency({
|
||||||
|
required this.title,
|
||||||
|
required this.hint,
|
||||||
|
required this.timesPerYear,
|
||||||
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
|
final String hint;
|
||||||
|
final double timesPerYear;
|
||||||
}
|
}
|
||||||
|
@ -1,59 +1,128 @@
|
|||||||
|
// Flutter
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
// Local
|
||||||
import '/models/expense.dart';
|
import '/models/expense.dart';
|
||||||
import '/models/frequency.dart';
|
import '/models/frequency.dart';
|
||||||
|
|
||||||
List<Expense> expenses = [];
|
List<Expense> expenses = [];
|
||||||
|
|
||||||
class ExpensePage extends StatelessWidget {
|
class ExpensePage extends StatefulWidget {
|
||||||
const ExpensePage({
|
const ExpensePage({
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ExpensePage> createState() => _ExpensePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ExpensePageState extends State<ExpensePage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListView.builder(
|
expenses.sort(
|
||||||
itemCount: expenses.length,
|
(a, b) => (b.calcComparableCost() - a.calcComparableCost()).toInt(),
|
||||||
itemBuilder: (_, index) {
|
|
||||||
final Expense curr = expenses[index];
|
|
||||||
return Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(4.0),
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
color: Colors.greenAccent,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(curr.name),
|
|
||||||
Text("${curr.cost.toString()} ${curr.frequency.title}"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
/*
|
|
||||||
return ListView(
|
|
||||||
children: [
|
|
||||||
ListTile(
|
|
||||||
title: Text("Fake Item 1"),
|
|
||||||
subtitle: Text("30.00 / month"),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
title: Text("Fake Item 2"),
|
|
||||||
subtitle: Text("180.00 / year"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
*/
|
return expenses.isEmpty
|
||||||
|
? Text("Add expenses to get started!")
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: expenses.length,
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
final Expense curr = expenses[index];
|
||||||
|
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: 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"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExpenseInputDialog extends StatefulWidget {
|
class ExpenseInputDialog extends StatefulWidget {
|
||||||
|
final Function() notifyParent;
|
||||||
const ExpenseInputDialog({
|
const ExpenseInputDialog({
|
||||||
super.key,
|
super.key,
|
||||||
|
required this.notifyParent,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -70,120 +139,137 @@ class _ExpenseInputDialogState extends State<ExpenseInputDialog> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const inputWidth = 300.0;
|
return Column(
|
||||||
const inputHeight = 50.0;
|
// prevent AlertDialog from taking full vertical height.
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
List<DropdownMenuItem> freqValues = [];
|
children: [
|
||||||
for (var freq in Frequency.values) {
|
Container(
|
||||||
freqValues.add(DropdownMenuItem(value: freq, child: Text(freq.title)));
|
alignment: FractionalOffset.topRight,
|
||||||
}
|
child: IconButton(
|
||||||
;
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
return AlertDialog(
|
},
|
||||||
title: Center(child: Text("Add New Expense")),
|
icon: Icon(Icons.clear),
|
||||||
content: Form(
|
),
|
||||||
key: _expenseFormKey,
|
),
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
AlertDialog(
|
||||||
child: Column(mainAxisSize: MainAxisSize.min, spacing: 10, children: [
|
insetPadding: EdgeInsets.all(0),
|
||||||
SizedBox(
|
title: Text("New Expense"),
|
||||||
width: inputWidth,
|
content: Form(
|
||||||
height: inputHeight,
|
key: _expenseFormKey,
|
||||||
child: TextFormField(
|
child: Column(
|
||||||
keyboardType: TextInputType.text,
|
mainAxisSize: MainAxisSize.min,
|
||||||
decoration: InputDecoration(
|
children: [
|
||||||
labelText: "Name",
|
TextFormField(
|
||||||
hintText: "Example: Red Pocket Phone Bill",
|
keyboardType: TextInputType.text,
|
||||||
),
|
decoration: InputDecoration(
|
||||||
validator: (value) {
|
labelText: "Name",
|
||||||
if (value!.isEmpty) {
|
hintText: "Example: Red Pocket",
|
||||||
return "Name must be provided.";
|
hintStyle: TextStyle(fontSize: 12.0),
|
||||||
}
|
errorStyle: TextStyle(fontSize: 10.0),
|
||||||
return null;
|
),
|
||||||
},
|
validator: (value) {
|
||||||
onSaved: (newValue) {
|
if (value!.isEmpty) {
|
||||||
_name = newValue!;
|
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!;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
actions: [
|
||||||
width: inputWidth,
|
Center(
|
||||||
height: inputHeight,
|
child: ElevatedButton.icon(
|
||||||
child: 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!);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: inputWidth,
|
|
||||||
height: inputHeight,
|
|
||||||
child: DropdownButtonFormField(
|
|
||||||
items: freqValues,
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: inputWidth,
|
|
||||||
height: inputHeight,
|
|
||||||
child: 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: [
|
|
||||||
SizedBox(
|
|
||||||
width: inputWidth,
|
|
||||||
height: inputHeight,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
print("TODO: Clear fields!");
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.cancel),
|
|
||||||
label: Text('Cancel'),
|
|
||||||
),
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
print("TODO: Save expense!");
|
|
||||||
if (_expenseFormKey.currentState!.validate()) {
|
if (_expenseFormKey.currentState!.validate()) {
|
||||||
_expenseFormKey.currentState!.save();
|
_expenseFormKey.currentState!.save();
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -195,19 +281,16 @@ class _ExpenseInputDialogState extends State<ExpenseInputDialog> {
|
|||||||
description: _desc),
|
description: _desc),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
print(expenses.toString());
|
widget.notifyParent();
|
||||||
for (var expense in expenses) {
|
|
||||||
print(expense.toString());
|
|
||||||
}
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.save),
|
icon: Icon(Icons.save),
|
||||||
label: Text('Submit'),
|
label: Text('Submit'),
|
||||||
),
|
),
|
||||||
],
|
)
|
||||||
),
|
],
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import sqflite_darwin
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
}
|
}
|
||||||
|
66
pubspec.lock
66
pubspec.lock
@ -131,6 +131,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.9.0"
|
||||||
|
platform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: platform
|
||||||
|
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.6"
|
||||||
|
plugin_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: plugin_platform_interface
|
||||||
|
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.8"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -144,6 +160,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.0"
|
version: "1.10.0"
|
||||||
|
sqflite:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: sqflite
|
||||||
|
sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
sqflite_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_android
|
||||||
|
sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
|
sqflite_common:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_common
|
||||||
|
sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.4+6"
|
||||||
|
sqflite_darwin:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_darwin
|
||||||
|
sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1+1"
|
||||||
|
sqflite_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_platform_interface
|
||||||
|
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -168,6 +224,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.3.0+3"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -202,4 +266,4 @@ packages:
|
|||||||
version: "14.3.0"
|
version: "14.3.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.6.1 <4.0.0"
|
dart: ">=3.6.1 <4.0.0"
|
||||||
flutter: ">=3.18.0-18.0.pre.54"
|
flutter: ">=3.24.0"
|
||||||
|
@ -9,6 +9,7 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
sqflite: ^2.4.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Reference in New Issue
Block a user