diff --git a/lib/domain/models/expense.dart b/lib/domain/models/expense.dart index cb5a4cf..596eb42 100644 --- a/lib/domain/models/expense.dart +++ b/lib/domain/models/expense.dart @@ -1,12 +1,14 @@ import 'package:wealth_wave/api/db/app_database.dart'; class Expense { + final int id; final double amount; final String? description; final List tags; final DateTime createdOn; Expense._({ + required this.id, required this.amount, required this.createdOn, required this.description, @@ -14,6 +16,7 @@ class Expense { }); factory Expense.from({required ExpenseDO expenseDO}) => Expense._( + id: expenseDO.id, amount: expenseDO.amount, createdOn: expenseDO.createdOn, description: expenseDO.description, diff --git a/lib/domain/services/expense_service.dart b/lib/domain/services/expense_service.dart index 1f29642..2e3909f 100644 --- a/lib/domain/services/expense_service.dart +++ b/lib/domain/services/expense_service.dart @@ -94,6 +94,14 @@ class ExpenseService { .getBy(id: id) .then((expenseDO) => Expense.from(expenseDO: expenseDO)); + Future> getExpensesForMonthDate({required final DateTime monthDate}) { + return _expenseApi + .getByMonth(monthDate: monthDate) + .then((expenseDOs) => expenseDOs + .map((expenseDO) => Expense.from(expenseDO: expenseDO)) + .toList()); + } + Future deleteBy({required final int id}) => _expenseApi.getBy(id: id).then((expenseDO) { DateTime monthDate = diff --git a/lib/ui/models/expense_vo.dart b/lib/ui/models/expense_vo.dart new file mode 100644 index 0000000..2be0f38 --- /dev/null +++ b/lib/ui/models/expense_vo.dart @@ -0,0 +1,25 @@ +import 'package:wealth_wave/domain/models/expense.dart'; + +class ExpenseVO { + final int id; + final double amount; + final String? description; + final DateTime createdOn; + final List tags; + + ExpenseVO._( + {required this.id, + required this.description, + required this.amount, + required this.createdOn, + required this.tags}); + + factory ExpenseVO.from({required final Expense expense}) { + return ExpenseVO._( + id: expense.id, + description: expense.description, + amount: expense.amount, + createdOn: expense.createdOn, + tags: expense.tags); + } +} diff --git a/lib/ui/pages/expense_page.dart b/lib/ui/pages/expense_page.dart index d483e3b..0f8b527 100644 --- a/lib/ui/pages/expense_page.dart +++ b/lib/ui/pages/expense_page.dart @@ -1,7 +1,13 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:pair/pair.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:wealth_wave/core/page_state.dart'; import 'package:wealth_wave/ui/presentation/expense_presenter.dart'; import 'package:wealth_wave/ui/widgets/create_expense_dialog.dart'; +import 'package:wealth_wave/ui/widgets/view_monthly_expenses_dialog.dart'; class ExpensePage extends StatefulWidget { const ExpensePage({super.key}); @@ -24,9 +30,18 @@ class _ExpensePage final BuildContext context, final ExpenseViewState snapshot) { Map monthlyExpenses = snapshot.monthlyExpenses; return Scaffold( - body: Center( - child: Text('Monthly Expenses: $monthlyExpenses', - style: Theme.of(context).textTheme.titleMedium)), + body: Column( + children: [ + Expanded( + flex: 1, + child: _showMonthlyExpenseGraph( + context: context, expenses: monthlyExpenses)), + Expanded( + flex: 1, + child: _showMonthlyExpenseList( + context: context, expenses: monthlyExpenses)), + ], + ), floatingActionButton: FloatingActionButton( onPressed: () { showCreateExpenseDialog(context: context).then((value) { @@ -43,4 +58,42 @@ class _ExpensePage ExpensePresenter initializePresenter() { return ExpensePresenter(); } + + Widget _showMonthlyExpenseGraph( + {required BuildContext context, + required Map expenses}) { + List> chartData = expenses.entries + .map((entry) => Pair(DateFormat('MMM').format(entry.key), entry.value)) + .toList(); + return SfCartesianChart( + primaryXAxis: const CategoryAxis(), + primaryYAxis: const NumericAxis(), + series: [ + ColumnSeries, String>( + dataSource: chartData, + xValueMapper: (Pair data, _) => data.key, + yValueMapper: (Pair data, _) => data.value, + name: 'Expenses', + ) + ], + ); + } + + Widget _showMonthlyExpenseList( + {required BuildContext context, + required Map expenses}) { + return ListView.builder( + itemCount: expenses.length, + itemBuilder: (BuildContext context, int index) { + final entry = expenses.entries.elementAt(index); + return ListTile( + title: Text(DateFormat('MMM').format(entry.key)), + subtitle: Text(entry.value.toString()), + onTap: () { + showViewMonthlyExpensesDialog(context: context, monthDate: entry.key); + }, + ); + }, + ); + } } diff --git a/lib/ui/presentation/monthly_expense_presenter.dart b/lib/ui/presentation/monthly_expense_presenter.dart new file mode 100644 index 0000000..54d97f0 --- /dev/null +++ b/lib/ui/presentation/monthly_expense_presenter.dart @@ -0,0 +1,33 @@ +import 'package:wealth_wave/core/presenter.dart'; +import 'package:wealth_wave/domain/services/expense_service.dart'; +import 'package:wealth_wave/ui/models/expense_vo.dart'; + +class MonthlyExpensePresenter extends Presenter { + final DateTime _monthDate; + final ExpenseService _expenseService; + + MonthlyExpensePresenter( + {required final DateTime monthDate, final ExpenseService? expenseService}) + : _monthDate = monthDate, + _expenseService = expenseService ?? ExpenseService(), + super(MonthlyExpenseViewState()); + + void getExpenses() { + _expenseService + .getExpensesForMonthDate(monthDate: _monthDate) + .then((expenses) => expenses + .map((expense) => ExpenseVO.from(expense: expense)) + .toList()) + .then((expenses) => updateViewState((viewState) { + viewState.expenses = expenses; + })); + } + + void deleteExpense({required final int id}) { + _expenseService.deleteBy(id: id).then((_) => getExpenses()); + } +} + +class MonthlyExpenseViewState { + List expenses = []; +} diff --git a/lib/ui/widgets/view_monthly_expenses_dialog.dart b/lib/ui/widgets/view_monthly_expenses_dialog.dart new file mode 100644 index 0000000..1c7b71c --- /dev/null +++ b/lib/ui/widgets/view_monthly_expenses_dialog.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:wealth_wave/core/page_state.dart'; +import 'package:wealth_wave/ui/app_dimen.dart'; +import 'package:wealth_wave/ui/models/expense_vo.dart'; +import 'package:wealth_wave/ui/presentation/monthly_expense_presenter.dart'; +import 'package:wealth_wave/ui/widgets/create_expense_dialog.dart'; +import 'package:wealth_wave/utils/ui_utils.dart'; + +Future showViewMonthlyExpensesDialog( + {required final BuildContext context, required final DateTime monthDate}) { + return showDialog( + context: context, + builder: (context) => _MonthlyExpensesDialog( + monthDate: monthDate, + )); +} + +class _MonthlyExpensesDialog extends StatefulWidget { + final DateTime monthDate; + + const _MonthlyExpensesDialog({required this.monthDate}); + + @override + State<_MonthlyExpensesDialog> createState() => _MonthlyExpensesPage(); +} + +class _MonthlyExpensesPage extends PageState { + @override + void initState() { + super.initState(); + presenter.getExpenses(); + } + + @override + Widget buildWidget(BuildContext context, MonthlyExpenseViewState snapshot) { + return AlertDialog( + title: const Text('Expenses'), + content: SizedBox( + width: double.maxFinite, + child: ListView.builder( + shrinkWrap: true, + itemCount: snapshot.expenses.length, + itemBuilder: (context, index) { + ExpenseVO expense = snapshot.expenses[index]; + return ListTile( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text(formatDate(expense.createdOn)), + const Text(' | '), + Text(expense.description ?? ''), + ], + ), + const SizedBox( + height: AppDimen.minPadding), // Add some spacing + Text('Amount: ${formatToCurrency(expense.amount)}'), + ], + ), + trailing: Row(mainAxisSize: MainAxisSize.min, children: [ + IconButton( + icon: const Icon(Icons.edit), + onPressed: () { + showCreateExpenseDialog( + context: context, expenseIdTOUpdate: expense.id) + .then((value) => presenter.getExpenses()); + }, + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + presenter.deleteExpense(id: expense.id); + }, + ) + ]), + ); + }, + )), + actions: [ + OutlinedButton( + child: const Text('Close'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + OutlinedButton( + child: const Text('Add Expense'), + onPressed: () { + showCreateExpenseDialog(context: context) + .then((value) => presenter.getExpenses()); + }, + ), + ], + ); + } + + @override + MonthlyExpensePresenter initializePresenter() { + return MonthlyExpensePresenter(monthDate: widget.monthDate); + } +}