// Flutter import 'package:flutter/material.dart'; // Local import '/models/expense.dart'; import '/models/frequency.dart'; List expenses = []; class ExpensePage extends StatefulWidget { const ExpensePage({ super.key, }); @override State createState() => _ExpensePageState(); } class _ExpensePageState extends State { refresh() { setState(() {}); } @override Widget build(BuildContext context) { final theme = Theme.of(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]; 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( 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(() { expenses.remove(curr); }); switch (direction) { case DismissDirection.startToEnd: // Only remove the item from the list. break; case DismissDirection.endToStart: // Open an edit dialog, then remove the item from the list. showDialog( context: context, builder: (_) => AlertDialog( content: ExpenseInputDialog( notifyParent: refresh, expense: curr, ), ), ); 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( 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, ), ), ), ], ), ), ), ), ); }, ); } } class ExpenseInputDialog extends StatefulWidget { final Function() notifyParent; final Expense? expense; const ExpenseInputDialog({ super.key, required this.notifyParent, this.expense, }); @override State createState() => _ExpenseInputDialogState(); } class _ExpenseInputDialogState extends State { final _expenseFormKey = GlobalKey(); String _name = ""; double _cost = 0; Frequency _freq = Frequency.monthly; String _desc = ""; @override Widget build(BuildContext context) { if (widget.expense != null) { _name = widget.expense!.name; _cost = widget.expense!.cost; _freq = widget.expense!.frequency; _desc = widget.expense!.description; } return Column( // prevent AlertDialog from taking full vertical height. mainAxisSize: MainAxisSize.min, children: [ Container( alignment: FractionalOffset.topRight, child: IconButton( onPressed: () { if (widget.expense != null) { setState(() { expenses.add(widget.expense!); widget.notifyParent(); }); } Navigator.of(context).pop(); }, icon: Icon(Icons.clear), ), ), AlertDialog( insetPadding: EdgeInsets.all(0), title: Center( child: widget.expense == null ? Text("New Expense") : Text("Edit 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: 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)) { 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: 10.0), errorStyle: TextStyle(fontSize: 10.0), ), initialValue: _cost != 0 ? _cost.toString() : "", 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: _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, decoration: InputDecoration( labelText: "Description", hintText: "Example: 1GB data, unlimited talk & text.", hintStyle: TextStyle(fontSize: 10.0), errorStyle: TextStyle(fontSize: 10.0), ), initialValue: _desc, 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'), ), ) ], ), ], ); } }