// Flutter import 'package:flutter/material.dart'; import 'package:flutter_expense_tracker/models/asset.dart'; import 'package:flutter_expense_tracker/models/income.dart'; // Local import '/models/tracked_item.dart'; import '/models/item_type.dart'; import '/models/expense.dart'; import '/models/frequency.dart'; import '/db.dart'; class TrackedItemPage extends StatefulWidget { final Future> assetsToLoad; final Function() notifyParent; const TrackedItemPage({ super.key, required this.assetsToLoad, required this.notifyParent, }); @override State createState() => _TrackedItemPageState(); } class _TrackedItemPageState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); return FutureBuilder>( future: widget.assetsToLoad, builder: (BuildContext context, AsyncSnapshot> snapshot) { if (!snapshot.hasData) { return Text('Loading...'); } snapshot.data!.sort( (a, b) => (b.calcComparableAmountYearly() - a.calcComparableAmountYearly()) .toInt(), ); return snapshot.data!.isEmpty ? Text( "Add items to get started.", softWrap: true, ) : ListView.builder( itemCount: snapshot.data!.length, itemBuilder: (_, index) { final TrackedItem curr = snapshot.data![index]; final itemKey = Key(curr.id!.toString()); final String itemTitle = curr.name; final String itemAmount; if (curr.frequency != null) { itemAmount = "${curr.amount.toStringAsFixed(2)} ${curr.frequency!.title}"; } else { itemAmount = curr.amount.toStringAsFixed(2); } final String itemDescription = curr.description; final double itemDayAmount, itemMonthAmount, itemYearAmount; final String estimateSymbolDaily, estimateSymbolMonthly, estimateSymbolYearly; if (curr.frequency != null) { 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 monthlyTitle = curr.type == ItemType.asset ? "" : " ${Frequency.monthly.title}"; final String itemTopText = itemDayAmount < 0 ? "" : "$estimateSymbolDaily${itemDayAmount.toStringAsFixed(2)} ${Frequency.daily.title}"; final String itemMiddleText = itemMonthAmount < 0 ? "" : "$estimateSymbolMonthly${itemMonthAmount.toStringAsFixed(2)}$monthlyTitle"; final String itemBottomText = itemYearAmount < 0 ? "" : "$estimateSymbolYearly${itemYearAmount.toStringAsFixed(2)} ${Frequency.yearly.title}"; return Padding( padding: const EdgeInsets.all(4.0), child: Dismissible( key: itemKey, background: Container( color: Colors.red, child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Icon(Icons.delete), Text("Delete"), ], ), ), secondaryBackground: Container( color: Colors.orange, child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Text("Edit"), Icon(Icons.edit), ], ), ), onDismissed: (direction) { setState(() { snapshot.data!.remove(curr); switch (direction) { case DismissDirection.startToEnd: // Remove the item from the database. if (curr is Expense) { DatabaseHelper.instance.removeExpense( curr.id!, ); } else if (curr is Income) { DatabaseHelper.instance.removeIncome( curr.id!, ); } else if (curr is Asset) { DatabaseHelper.instance.removeAsset( curr.id!, ); } else { throw UnimplementedError( "Cannot remove unimplemented item type."); } break; case DismissDirection.endToStart: // Open an edit dialog, then remove the item from the list. showDialog( context: context, builder: (_) => AlertDialog( content: TrackedItemInputDialog( notifyParent: widget.notifyParent, entry: curr, amountText: curr.getAmountText(), type: curr.type!, ), ), ); break; default: UnimplementedError( "Direction ${direction.toString()} not recognized.", ); } }); }, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), color: theme.colorScheme.onPrimary, ), child: Padding( padding: const EdgeInsets.all(4.0), child: Row( mainAxisSize: MainAxisSize.max, children: [ Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( itemTitle, style: TextStyle(fontSize: 20.0), ), Text( itemAmount, style: TextStyle(fontSize: 12.0), ), ], ), Expanded( child: Center( child: Text( itemDescription, style: TextStyle( fontSize: 12.0, ), softWrap: true, textAlign: TextAlign.center, ), ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( itemTopText, style: TextStyle(fontSize: 12.0), ), Text( itemMiddleText, style: TextStyle(fontSize: 12.0), ), Text( itemBottomText, style: TextStyle(fontSize: 12.0), ), ], ), ], ), ), ), ), ); }, ); }); } } class TrackedItemInputDialog extends StatefulWidget { final Function() notifyParent; final TrackedItem? entry; final String? amountText; final ItemType? type; const TrackedItemInputDialog({ super.key, required this.notifyParent, this.entry, this.amountText, this.type, }); @override State createState() => _TrackedItemInputDialogState(); } class _TrackedItemInputDialogState extends State { final _formKey = GlobalKey(); int? _id; String _name = ""; double _amount = 0; Frequency _freq = Frequency.monthly; String _desc = ""; ItemType? _type; @override Widget build(BuildContext context) { if (widget.type == null && (widget.entry != null && widget.entry!.type == null)) { throw FlutterError("No ItemType provided for TrackedItemInputDialog."); } _type = widget.type; if (widget.entry != null) { _id = widget.entry!.id; _name = widget.entry!.name; _amount = widget.entry!.amount; widget.entry!.frequency == null ? null : _freq = widget.entry!.frequency!; _desc = widget.entry!.description; _type = widget.entry!.type!; } String amountText = widget.amountText != null ? widget.amountText! : TrackedItem.amountText; return Column( // prevent AlertDialog from taking full vertical height. mainAxisSize: MainAxisSize.min, children: [ Container( alignment: FractionalOffset.topRight, child: IconButton( onPressed: () { if (widget.entry != null) { setState(() { switch (_type) { case ItemType.expense: DatabaseHelper.instance.addExpense(widget.entry!); break; case ItemType.income: DatabaseHelper.instance.addIncome(widget.entry!); break; case ItemType.asset: DatabaseHelper.instance.addAsset(widget.entry!); break; default: throw UnimplementedError( "Cannot add unimplemented type."); } widget.notifyParent(); }); } Navigator.of(context).pop(); }, icon: Icon(Icons.clear), ), ), AlertDialog( insetPadding: EdgeInsets.all(0), title: Center( child: widget.entry == null ? Text("New ${_type!.title}") : Text("Edit ${_type!.title}"), ), content: FutureBuilder>( future: DatabaseHelper.instance.getExpenses(), builder: (BuildContext context, AsyncSnapshot> snapshot) { if (!snapshot.hasData) { return Center(child: Text('Loading...')); } List expenses = snapshot.data!; return Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( keyboardType: TextInputType.text, textCapitalization: TextCapitalization.words, decoration: InputDecoration( labelText: "Name", hintText: "Example: Red Pocket", hintStyle: TextStyle(fontSize: 10.0), errorStyle: TextStyle(fontSize: 10.0), ), initialValue: _name, validator: (value) { if (value!.isEmpty) { return "Name must be provided."; } if (!expenses.every((expense) => expense.name != value || expense.id == _id)) { return "Name must be unique, already in use."; } return null; }, onSaved: (value) { _name = value!; }, ), TextFormField( keyboardType: TextInputType.numberWithOptions(decimal: true), decoration: InputDecoration( labelText: amountText, hintText: "Example: 10.00", hintStyle: TextStyle(fontSize: 10.0), errorStyle: TextStyle(fontSize: 10.0), ), initialValue: _amount != 0 ? _amount.toString() : "", validator: (value) { if (value == null || value.isEmpty) { return "$amountText must be provided."; } if (double.tryParse(value) == null) { return "$amountText must be a valid number."; } if (double.parse(value) < 0) { return "Please use the Income page rather than having negative expenses."; } if (double.parse(value) < 0.01) { return "$amountText must be one hundreth (0.01) or higher."; } return null; }, onSaved: (value) { _amount = double.parse(value!); }, ), if (_type != ItemType.asset) 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: _freq, 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, textCapitalization: TextCapitalization.sentences, decoration: InputDecoration( labelText: "Description", hintText: "Example: 1GB data with unlimited talk & text.", hintStyle: TextStyle(fontSize: 8.0), errorStyle: TextStyle(fontSize: 10.0), ), initialValue: _desc, validator: (value) { return null; }, onSaved: (value) { _desc = value!; }, ), ], ), ); }), actions: [ Center( child: ElevatedButton.icon( onPressed: () { if (_formKey.currentState!.validate()) { _formKey.currentState!.save(); setState( () { switch (_type) { case ItemType.expense: Expense expense = Expense( id: _id, name: _name, amount: _amount, frequency: _freq, description: _desc, ); if (_id != null) { DatabaseHelper.instance.updateExpense( expense, ); } else { DatabaseHelper.instance.addExpense( expense, ); } break; case ItemType.income: Income income = Income( id: _id, name: _name, amount: _amount, frequency: _freq, description: _desc, ); if (_id != null) { DatabaseHelper.instance.updateIncome( income, ); } else { DatabaseHelper.instance.addIncome( income, ); } break; case ItemType.asset: Asset asset = Asset( id: _id, name: _name, amount: _amount, description: _desc, ); if (_id != null) { DatabaseHelper.instance.updateAsset( asset, ); } else { DatabaseHelper.instance.addAsset( asset, ); } break; default: throw UnimplementedError( "No code for type ${_type!.title}", ); } widget.notifyParent(); Navigator.of(context).pop(); }, ); } }, icon: Icon(Icons.save), label: Text('Submit'), ), ) ], ), ], ); } }