// Helpful guides: // - https://flutter.dev/docs/cookbook/forms/validation import 'package:flutter/material.dart'; void main() { runApp(const MainApp()); } // https://www.tutorialspoint.com/dart_programming/dart_programming_enumeration.htm enum Recurrence { daily, weekly, biweekly, montly, yearly } class Expense { String name; double cost; Recurrence recurrence; String description; Expense(this.name, this.cost, this.recurrence, this.description); } class MainApp extends StatelessWidget { const MainApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Recurring Expense Tracker', theme: ThemeData( useMaterial3: true, colorScheme: ColorScheme.fromSeed(seedColor: Colors.green), ), home: HomePage(), ); } } class HomePage extends StatefulWidget { const HomePage({ super.key, }); @override State createState() => _HomePageState(); } class _HomePageState extends State { var pageSelected = 0; @override Widget build(BuildContext context) { Widget page; Widget? dialog; switch (pageSelected) { case 0: page = ExpensePage(); dialog = ExpenseInputDialog(); case 1: page = IncomePage(); case 2: page = AssetPage(); case 3: page = ProjectionPage(); case 4: page = SettingsPage(); default: throw UnimplementedError('No widget for page $pageSelected yet!'); } Future addNewValue(BuildContext context) { return showDialog( context: context, builder: (_) => AlertDialog(content: dialog)); } Widget? floatingButton; if (dialog != null) { floatingButton = IconButton( onPressed: () { addNewValue(context); }, icon: Icon(Icons.add), color: Theme.of(context).colorScheme.onSurface, ); } return LayoutBuilder(builder: (context, constraints) { return Scaffold( body: Row( children: [ SafeArea( child: NavigationRail( extended: constraints.maxWidth >= 800, destinations: [ 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( child: Container( color: Theme.of(context).colorScheme.primaryContainer, child: page, ), ), ], ), floatingActionButton: floatingButton, ); }); } } class ExpensePage extends StatelessWidget { const ExpensePage({ super.key, }); @override Widget build(BuildContext context) { 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"), ), ], ); } } class ExpenseInputDialog extends StatefulWidget { const ExpenseInputDialog({ super.key, }); @override State createState() => _ExpenseInputDialogState(); } class _ExpenseInputDialogState extends State { final _expenseFormKey = GlobalKey(); @override Widget build(BuildContext context) { const inputWidth = 300.0; const inputHeight = 50.0; const recurrenceValues = [ DropdownMenuItem(value: Recurrence.daily, child: Text("Daily")), DropdownMenuItem(value: Recurrence.weekly, child: Text("Weekly")), DropdownMenuItem(value: Recurrence.biweekly, child: Text("Biweekly")), DropdownMenuItem(value: Recurrence.montly, child: Text("Monthly")), DropdownMenuItem( value: Recurrence.yearly, child: Text( "Yearly", )), ]; return AlertDialog( title: Center(child: Text("Add New Expense")), content: Form( key: _expenseFormKey, autovalidateMode: AutovalidateMode.onUserInteraction, child: Column(mainAxisSize: MainAxisSize.min, spacing: 10, children: [ SizedBox( width: inputWidth, height: inputHeight, child: TextFormField( keyboardType: TextInputType.text, decoration: InputDecoration( labelText: "Name", hintText: "Example: Red Pocket Phone Bill", ), validator: (value) { if (value!.isEmpty) { return "Name must be provided."; } return null; }, onChanged: (newValue) { // _expenseFormKey.currentState!.reset(); }, )), SizedBox( width: inputWidth, height: inputHeight, 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; }, onChanged: (newValue) { //_expenseFormKey.currentState!.reset(); }, ), ), SizedBox( width: inputWidth, height: inputHeight, child: DropdownButtonFormField( items: recurrenceValues, decoration: InputDecoration( labelText: "Recurrence", hintText: "Example: Monthly"), validator: (value) { if (value == null) { return "Recurrence must be provided."; } return null; }, onChanged: (newValue) { //_expenseFormKey.currentState!.reset(); }, ), ), 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; }, onChanged: (newValue) { //_expenseFormKey.currentState!.reset(); }, )), ]), ), actions: [ SizedBox( width: inputWidth, height: inputHeight, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, children: [ ElevatedButton.icon( 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()) { _expenseFormKey.currentState!.save(); Navigator.of(context).pop(); } }, icon: Icon(Icons.save), label: Text('Submit'), ), ], )) ], ); } } class IncomePage extends StatelessWidget { const IncomePage({ super.key, }); @override Widget build(BuildContext context) { return Center( child: Column( children: [ Text("TBD"), Placeholder(), ], )); } } class AssetPage extends StatelessWidget { const AssetPage({ super.key, }); @override Widget build(BuildContext context) { return Placeholder(); } } class ProjectionPage extends StatelessWidget { const ProjectionPage({ super.key, }); @override Widget build(BuildContext context) { return Placeholder(); } } class SettingsPage extends StatelessWidget { const SettingsPage({ super.key, }); @override Widget build(BuildContext context) { return Placeholder(); } }