@marcos_feitoza/personal-finance-frontend-feature-dashboard 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/lib/screens/home_screen.dart +192 -291
- package/lib/viewmodels/dashboard_viewmodel.dart +5 -0
- package/package.json +1 -1
- package/pubspec.yaml +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
# [1.2.0](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/compare/v1.1.2...v1.2.0) (2026-01-29)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* add revoke token ([9d4febb](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/commit/9d4febb18f932dae3c853656cb08ba9e3ab53ca5))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* add useer profile ([f3e410a](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/commit/f3e410aea7852115e5be40199d3cd24e95205812))
|
|
12
|
+
* new AppTheme process ([5538715](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/commit/55387150f8271f898e8309dfc0c3f1dc8ee66661))
|
|
13
|
+
|
|
1
14
|
## [1.1.2](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/compare/v1.1.1...v1.1.2) (2025-12-06)
|
|
2
15
|
|
|
3
16
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import 'package:flutter/material.dart';
|
|
2
|
-
import 'package:flutter/services.dart';
|
|
3
2
|
import 'package:personal_finance_frontend_core_ui/widgets/salary_form.dart';
|
|
4
3
|
import 'package:personal_finance_frontend_core_ui/widgets/expense_form.dart';
|
|
5
4
|
import 'package:personal_finance_frontend_core_ui/widgets/investment_form.dart';
|
|
@@ -14,24 +13,16 @@ import 'package:personal_finance_frontend_feature_reports/screens/move_money_rep
|
|
|
14
13
|
import 'package:personal_finance_frontend_feature_investments/screens/investment_account_screen.dart';
|
|
15
14
|
import 'package:personal_finance_frontend_feature_investments/screens/rrsp_sun_life_screen.dart';
|
|
16
15
|
import 'package:personal_finance_frontend_feature_investments/screens/crypto_account_screen.dart';
|
|
17
|
-
import 'package:personal_finance_frontend_feature_charts/viewmodels/salary_chart_viewmodel.dart';
|
|
18
|
-
import 'package:personal_finance_frontend_feature_reports/viewmodels/expense_report_viewmodel.dart';
|
|
19
|
-
import 'package:personal_finance_frontend_core_services/services/transaction_service.dart';
|
|
20
|
-
import 'package:personal_finance_frontend_core_ui/utils/currency_input_formatter.dart';
|
|
21
|
-
import 'package:personal_finance_frontend_core_services/models/payment_method.dart';
|
|
22
16
|
import 'package:intl/intl.dart';
|
|
23
17
|
import 'package:personal_finance_frontend_feature_management/screens/reconciliation_screen.dart';
|
|
24
18
|
import 'package:personal_finance_frontend_core_ui/widgets/app_widgets.dart';
|
|
25
19
|
import 'package:personal_finance_frontend_core_ui/utils/theme_notifier.dart';
|
|
26
20
|
import 'package:personal_finance_frontend_core_services/providers/auth_provider.dart';
|
|
27
21
|
import 'package:personal_finance_frontend_core_ui/widgets/user_profile_avatar.dart';
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@override
|
|
33
|
-
_HomeScreenState createState() => _HomeScreenState();
|
|
34
|
-
}
|
|
22
|
+
import 'package:personal_finance_frontend_feature_profile/screens/profile_screen.dart';
|
|
23
|
+
import '../viewmodels/dashboard_viewmodel.dart';
|
|
24
|
+
import 'package:personal_finance_frontend_core_ui/utils/currency_input_formatter.dart';
|
|
25
|
+
import 'package:personal_finance_frontend_core_ui/theme/app_colors.dart';
|
|
35
26
|
|
|
36
27
|
final String currencySymbol = r'$';
|
|
37
28
|
|
|
@@ -39,131 +30,174 @@ String _formatCurrency(double value) {
|
|
|
39
30
|
final format = NumberFormat.currency(
|
|
40
31
|
symbol: currencySymbol,
|
|
41
32
|
decimalDigits: 2,
|
|
42
|
-
locale: 'en_US',
|
|
33
|
+
locale: 'en_US',
|
|
43
34
|
);
|
|
44
35
|
return format.format(value);
|
|
45
36
|
}
|
|
46
37
|
|
|
47
|
-
class
|
|
48
|
-
|
|
49
|
-
final _expenseFormKey = GlobalKey<FormState>();
|
|
50
|
-
final _investmentFormKey = GlobalKey<FormState>();
|
|
51
|
-
|
|
52
|
-
final _rbcCashController = TextEditingController();
|
|
53
|
-
final _wealthsimpleCashController = TextEditingController();
|
|
54
|
-
final _rbcCardController = TextEditingController();
|
|
55
|
-
final _bmoCardController = TextEditingController();
|
|
56
|
-
|
|
57
|
-
final TransactionService _transactionService = TransactionService();
|
|
58
|
-
double _totalIncome = 0.0;
|
|
59
|
-
double _totalExpenses = 0.0;
|
|
60
|
-
double _rbcUnpaidExpenses = 0.0;
|
|
61
|
-
double _bmoUnpaidExpenses = 0.0;
|
|
62
|
-
|
|
63
|
-
List<Map<String, dynamic>> _cashTransactions = [];
|
|
64
|
-
List<Map<String, dynamic>> _rbcTransactions = [];
|
|
65
|
-
List<Map<String, dynamic>> _bmoTransactions = [];
|
|
66
|
-
List<Map<String, dynamic>> _investments = [];
|
|
67
|
-
Map<String, double> _accountBalances = {};
|
|
68
|
-
|
|
69
|
-
bool _isLoading = true;
|
|
70
|
-
String? _selectedMenu;
|
|
71
|
-
|
|
72
|
-
final List<String> _investmentAccountNames = [
|
|
73
|
-
'TFSA',
|
|
74
|
-
'RRSP Wealthsimple',
|
|
75
|
-
'Non-Registered',
|
|
76
|
-
'Crypto'
|
|
77
|
-
];
|
|
78
|
-
|
|
79
|
-
@override
|
|
80
|
-
void initState() {
|
|
81
|
-
super.initState();
|
|
82
|
-
// Fetch data after the first frame is built
|
|
83
|
-
WidgetsBinding.instance.addPostFrameCallback((_) => _fetchData());
|
|
84
|
-
}
|
|
38
|
+
class HomeScreen extends StatelessWidget {
|
|
39
|
+
const HomeScreen({Key? key}) : super(key: key);
|
|
85
40
|
|
|
86
41
|
@override
|
|
87
|
-
|
|
88
|
-
_rbcCashController.dispose();
|
|
89
|
-
_wealthsimpleCashController.dispose();
|
|
90
|
-
_rbcCardController.dispose();
|
|
91
|
-
_bmoCardController.dispose();
|
|
92
|
-
super.dispose();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
Future<void> _fetchData() async {
|
|
96
|
-
if (!mounted) return;
|
|
42
|
+
Widget build(BuildContext context) {
|
|
97
43
|
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
|
98
44
|
final token = authProvider.token;
|
|
99
45
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
46
|
+
return ChangeNotifierProvider(
|
|
47
|
+
create: (_) => DashboardViewModel(token),
|
|
48
|
+
child: Consumer<DashboardViewModel>(
|
|
49
|
+
builder: (context, viewModel, child) {
|
|
50
|
+
return Scaffold(
|
|
51
|
+
appBar: AppBar(
|
|
52
|
+
title: const Text('Personal Finance Dashboard'),
|
|
53
|
+
actions: [
|
|
54
|
+
PopupMenuButton<String>(
|
|
55
|
+
onSelected: (value) {
|
|
56
|
+
if (value == 'profile') {
|
|
57
|
+
Navigator.push(
|
|
58
|
+
context,
|
|
59
|
+
MaterialPageRoute(
|
|
60
|
+
builder: (context) => const ProfileScreen()),
|
|
61
|
+
);
|
|
62
|
+
} else if (value == 'theme') {
|
|
63
|
+
final themeNotifier =
|
|
64
|
+
Provider.of<ThemeNotifier>(context, listen: false);
|
|
65
|
+
final newTheme =
|
|
66
|
+
themeNotifier.themeMode == ThemeMode.dark
|
|
67
|
+
? ThemeMode.light
|
|
68
|
+
: ThemeMode.dark;
|
|
69
|
+
themeNotifier.setThemeMode(newTheme);
|
|
70
|
+
} else if (value == 'logout') {
|
|
71
|
+
Provider.of<AuthProvider>(context, listen: false)
|
|
72
|
+
.logout();
|
|
73
|
+
Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
|
|
77
|
+
const PopupMenuItem<String>(
|
|
78
|
+
value: 'profile',
|
|
79
|
+
child: Text('My Profile'),
|
|
80
|
+
),
|
|
81
|
+
const PopupMenuItem<String>(
|
|
82
|
+
value: 'theme',
|
|
83
|
+
child: Text('Toggle Theme'),
|
|
84
|
+
),
|
|
85
|
+
const PopupMenuItem<String>(
|
|
86
|
+
value: 'logout',
|
|
87
|
+
child: Text('Logout'),
|
|
88
|
+
),
|
|
89
|
+
],
|
|
90
|
+
child: const UserProfileAvatar(),
|
|
91
|
+
),
|
|
92
|
+
const SizedBox(width: 10),
|
|
93
|
+
],
|
|
94
|
+
),
|
|
95
|
+
drawer: Drawer(
|
|
96
|
+
child: viewModel.selectedMenu == null
|
|
97
|
+
? _buildMainMenu(context, viewModel)
|
|
98
|
+
: _buildSubMenu(context, viewModel),
|
|
99
|
+
),
|
|
100
|
+
body: viewModel.isLoading
|
|
101
|
+
? const Center(child: CircularProgressIndicator())
|
|
102
|
+
: SingleChildScrollView(
|
|
103
|
+
padding: const EdgeInsets.all(16.0),
|
|
104
|
+
child: Column(
|
|
105
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
106
|
+
children: [
|
|
107
|
+
Row(
|
|
108
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
109
|
+
children: [
|
|
110
|
+
Expanded(
|
|
111
|
+
child: _buildCashBalanceCard(
|
|
112
|
+
context, viewModel)),
|
|
113
|
+
const SizedBox(width: 16),
|
|
114
|
+
Expanded(
|
|
115
|
+
child: _buildCreditCardBalanceCard(
|
|
116
|
+
context,
|
|
117
|
+
viewModel,
|
|
118
|
+
'RBC Balance',
|
|
119
|
+
viewModel.rbcCardController,
|
|
120
|
+
viewModel.rbcUnpaidExpenses)),
|
|
121
|
+
const SizedBox(width: 16),
|
|
122
|
+
Expanded(
|
|
123
|
+
child: _buildCreditCardBalanceCard(
|
|
124
|
+
context,
|
|
125
|
+
viewModel,
|
|
126
|
+
'BMO Balance',
|
|
127
|
+
viewModel.bmoCardController,
|
|
128
|
+
viewModel.bmoUnpaidExpenses)),
|
|
129
|
+
],
|
|
130
|
+
),
|
|
131
|
+
const SizedBox(height: 16),
|
|
132
|
+
Row(
|
|
133
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
134
|
+
children: [
|
|
135
|
+
Expanded(
|
|
136
|
+
child: _buildRecentTransactionsList(
|
|
137
|
+
'Cash Transactions',
|
|
138
|
+
viewModel.cashTransactions)),
|
|
139
|
+
const SizedBox(width: 16),
|
|
140
|
+
Expanded(
|
|
141
|
+
child: _buildRecentTransactionsList(
|
|
142
|
+
'RBC Transactions',
|
|
143
|
+
viewModel.rbcTransactions)),
|
|
144
|
+
const SizedBox(width: 16),
|
|
145
|
+
Expanded(
|
|
146
|
+
child: _buildRecentTransactionsList(
|
|
147
|
+
'BMO Transactions',
|
|
148
|
+
viewModel.bmoTransactions)),
|
|
149
|
+
const SizedBox(width: 16),
|
|
150
|
+
Expanded(
|
|
151
|
+
child: ExpenseForm(
|
|
152
|
+
formKey: viewModel.expenseFormKey,
|
|
153
|
+
onTransactionSuccess: viewModel.fetchData,
|
|
154
|
+
token: token,
|
|
155
|
+
)),
|
|
156
|
+
],
|
|
157
|
+
),
|
|
158
|
+
const SizedBox(height: 16),
|
|
159
|
+
Row(
|
|
160
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
161
|
+
children: [
|
|
162
|
+
Expanded(
|
|
163
|
+
child: InvestmentForm(
|
|
164
|
+
formKey: viewModel.investmentFormKey,
|
|
165
|
+
accountBalances: viewModel.accountBalances,
|
|
166
|
+
cashBalance: viewModel.appCalculatedCash,
|
|
167
|
+
onTransactionSuccess: viewModel.fetchData,
|
|
168
|
+
token: token,
|
|
169
|
+
)),
|
|
170
|
+
const SizedBox(width: 16),
|
|
171
|
+
Expanded(
|
|
172
|
+
child: SalaryForm(
|
|
173
|
+
formKey: viewModel.salaryFormKey,
|
|
174
|
+
onTransactionSuccess: viewModel.fetchData,
|
|
175
|
+
token: token,
|
|
176
|
+
)),
|
|
177
|
+
],
|
|
178
|
+
),
|
|
179
|
+
],
|
|
180
|
+
),
|
|
181
|
+
),
|
|
182
|
+
floatingActionButton: FloatingActionButton(
|
|
183
|
+
onPressed: viewModel.fetchData,
|
|
184
|
+
tooltip: 'Refresh Data',
|
|
185
|
+
child: const Icon(Icons.refresh),
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
},
|
|
189
|
+
),
|
|
190
|
+
);
|
|
159
191
|
}
|
|
160
192
|
|
|
161
|
-
Widget _buildMainMenu(BuildContext context) {
|
|
193
|
+
Widget _buildMainMenu(BuildContext context, DashboardViewModel viewModel) {
|
|
162
194
|
return ListView(
|
|
163
195
|
padding: EdgeInsets.zero,
|
|
164
196
|
children: <Widget>[
|
|
165
197
|
const DrawerHeader(
|
|
166
198
|
decoration: BoxDecoration(
|
|
199
|
+
// TODO(UI): replace hardcoded color with a theme token (e.g. theme.colorScheme.primary)
|
|
200
|
+
// or a semantic token in AppColors if needed.
|
|
167
201
|
color: Colors.blue,
|
|
168
202
|
),
|
|
169
203
|
child: Text(
|
|
@@ -177,53 +211,37 @@ class _HomeScreenState extends State<HomeScreen> {
|
|
|
177
211
|
ListTile(
|
|
178
212
|
leading: const Icon(Icons.assessment),
|
|
179
213
|
title: const Text('Investments'),
|
|
180
|
-
onTap: ()
|
|
181
|
-
setState(() {
|
|
182
|
-
_selectedMenu = 'investments';
|
|
183
|
-
});
|
|
184
|
-
},
|
|
214
|
+
onTap: () => viewModel.setMenu('investments'),
|
|
185
215
|
),
|
|
186
216
|
ListTile(
|
|
187
217
|
leading: const Icon(Icons.description),
|
|
188
218
|
title: const Text('Reports'),
|
|
189
|
-
onTap: ()
|
|
190
|
-
setState(() {
|
|
191
|
-
_selectedMenu = 'reports';
|
|
192
|
-
});
|
|
193
|
-
},
|
|
219
|
+
onTap: () => viewModel.setMenu('reports'),
|
|
194
220
|
),
|
|
195
221
|
ListTile(
|
|
196
222
|
leading: const Icon(Icons.bar_chart),
|
|
197
223
|
title: const Text('Charts'),
|
|
198
|
-
onTap: ()
|
|
199
|
-
setState(() {
|
|
200
|
-
_selectedMenu = 'charts';
|
|
201
|
-
});
|
|
202
|
-
},
|
|
224
|
+
onTap: () => viewModel.setMenu('charts'),
|
|
203
225
|
),
|
|
204
226
|
ListTile(
|
|
205
227
|
leading: const Icon(Icons.settings),
|
|
206
228
|
title: const Text('Management'),
|
|
207
|
-
onTap: ()
|
|
208
|
-
setState(() {
|
|
209
|
-
_selectedMenu = 'management';
|
|
210
|
-
});
|
|
211
|
-
},
|
|
229
|
+
onTap: () => viewModel.setMenu('management'),
|
|
212
230
|
),
|
|
213
231
|
],
|
|
214
232
|
);
|
|
215
233
|
}
|
|
216
234
|
|
|
217
|
-
Widget _buildSubMenu(BuildContext context) {
|
|
235
|
+
Widget _buildSubMenu(BuildContext context, DashboardViewModel viewModel) {
|
|
218
236
|
List<Widget> items;
|
|
219
237
|
String title;
|
|
220
238
|
|
|
221
|
-
switch (
|
|
239
|
+
switch (viewModel.selectedMenu) {
|
|
222
240
|
case 'investments':
|
|
223
241
|
title = 'Investments';
|
|
224
242
|
items = [
|
|
225
|
-
...
|
|
226
|
-
final balance =
|
|
243
|
+
...viewModel.investmentAccountNames.map((accountName) {
|
|
244
|
+
final balance = viewModel.accountBalances[accountName] ?? 0.0;
|
|
227
245
|
final formattedBalance = _formatCurrency(balance);
|
|
228
246
|
return ListTile(
|
|
229
247
|
title: Text(' $accountName - $formattedBalance'),
|
|
@@ -350,7 +368,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|
|
350
368
|
context,
|
|
351
369
|
MaterialPageRoute(
|
|
352
370
|
builder: (context) => const ManageTransactionsScreen()),
|
|
353
|
-
).then((_) =>
|
|
371
|
+
).then((_) => viewModel.fetchData());
|
|
354
372
|
},
|
|
355
373
|
),
|
|
356
374
|
ListTile(
|
|
@@ -380,11 +398,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|
|
380
398
|
backgroundColor: Theme.of(context).primaryColor,
|
|
381
399
|
leading: IconButton(
|
|
382
400
|
icon: const Icon(Icons.arrow_back),
|
|
383
|
-
onPressed: ()
|
|
384
|
-
setState(() {
|
|
385
|
-
_selectedMenu = null;
|
|
386
|
-
});
|
|
387
|
-
},
|
|
401
|
+
onPressed: () => viewModel.setMenu(null),
|
|
388
402
|
),
|
|
389
403
|
automaticallyImplyLeading: false,
|
|
390
404
|
),
|
|
@@ -393,129 +407,10 @@ class _HomeScreenState extends State<HomeScreen> {
|
|
|
393
407
|
);
|
|
394
408
|
}
|
|
395
409
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
final double
|
|
399
|
-
|
|
400
|
-
CurrencyInputFormatter.unformat(_wealthsimpleCashController.text);
|
|
401
|
-
|
|
402
|
-
final double appCalculatedCash = _accountBalances['CASH'] ?? 0.0;
|
|
403
|
-
final token = Provider.of<AuthProvider>(context, listen: false).token;
|
|
404
|
-
|
|
405
|
-
return Scaffold(
|
|
406
|
-
appBar: AppBar(
|
|
407
|
-
title: const Text('Personal Finance Dashboard'),
|
|
408
|
-
actions: [
|
|
409
|
-
const UserProfileAvatar(),
|
|
410
|
-
Consumer<ThemeNotifier>(
|
|
411
|
-
builder: (context, themeNotifier, child) {
|
|
412
|
-
return IconButton(
|
|
413
|
-
icon: Icon(themeNotifier.themeMode == ThemeMode.dark
|
|
414
|
-
? Icons.dark_mode
|
|
415
|
-
: Icons.light_mode),
|
|
416
|
-
onPressed: () {
|
|
417
|
-
themeNotifier.setThemeMode(
|
|
418
|
-
themeNotifier.themeMode == ThemeMode.dark
|
|
419
|
-
? ThemeMode.light
|
|
420
|
-
: ThemeMode.dark);
|
|
421
|
-
},
|
|
422
|
-
);
|
|
423
|
-
},
|
|
424
|
-
),
|
|
425
|
-
IconButton(
|
|
426
|
-
icon: const Icon(Icons.logout),
|
|
427
|
-
onPressed: () {
|
|
428
|
-
Provider.of<AuthProvider>(context, listen: false).logout();
|
|
429
|
-
},
|
|
430
|
-
),
|
|
431
|
-
],
|
|
432
|
-
),
|
|
433
|
-
drawer: Drawer(
|
|
434
|
-
child: _selectedMenu == null
|
|
435
|
-
? _buildMainMenu(context)
|
|
436
|
-
: _buildSubMenu(context),
|
|
437
|
-
),
|
|
438
|
-
body: _isLoading
|
|
439
|
-
? const Center(child: CircularProgressIndicator())
|
|
440
|
-
: SingleChildScrollView(
|
|
441
|
-
padding: const EdgeInsets.all(16.0),
|
|
442
|
-
child: Column(
|
|
443
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
444
|
-
children: [
|
|
445
|
-
Row(
|
|
446
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
447
|
-
children: [
|
|
448
|
-
Expanded(
|
|
449
|
-
child: _buildCashBalanceCard(
|
|
450
|
-
totalUserCash, appCalculatedCash)),
|
|
451
|
-
const SizedBox(width: 16),
|
|
452
|
-
Expanded(
|
|
453
|
-
child: _buildCreditCardBalanceCard('RBC Balance',
|
|
454
|
-
_rbcCardController, _rbcUnpaidExpenses)),
|
|
455
|
-
const SizedBox(width: 16),
|
|
456
|
-
Expanded(
|
|
457
|
-
child: _buildCreditCardBalanceCard('BMO Balance',
|
|
458
|
-
_bmoCardController, _bmoUnpaidExpenses)),
|
|
459
|
-
],
|
|
460
|
-
),
|
|
461
|
-
const SizedBox(height: 16),
|
|
462
|
-
Row(
|
|
463
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
464
|
-
children: [
|
|
465
|
-
Expanded(
|
|
466
|
-
child: _buildRecentTransactionsList(
|
|
467
|
-
'Cash Transactions', _cashTransactions)),
|
|
468
|
-
const SizedBox(width: 16),
|
|
469
|
-
Expanded(
|
|
470
|
-
child: _buildRecentTransactionsList(
|
|
471
|
-
'RBC Transactions', _rbcTransactions)),
|
|
472
|
-
const SizedBox(width: 16),
|
|
473
|
-
Expanded(
|
|
474
|
-
child: _buildRecentTransactionsList(
|
|
475
|
-
'BMO Transactions', _bmoTransactions)),
|
|
476
|
-
const SizedBox(width: 16),
|
|
477
|
-
Expanded(
|
|
478
|
-
child: ExpenseForm(
|
|
479
|
-
formKey: _expenseFormKey,
|
|
480
|
-
onTransactionSuccess: _fetchData,
|
|
481
|
-
token: token,
|
|
482
|
-
)),
|
|
483
|
-
],
|
|
484
|
-
),
|
|
485
|
-
const SizedBox(height: 16),
|
|
486
|
-
Row(
|
|
487
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
488
|
-
children: [
|
|
489
|
-
Expanded(
|
|
490
|
-
child: InvestmentForm(
|
|
491
|
-
formKey: _investmentFormKey,
|
|
492
|
-
accountBalances: _accountBalances,
|
|
493
|
-
cashBalance: appCalculatedCash,
|
|
494
|
-
onTransactionSuccess: _fetchData,
|
|
495
|
-
token: token,
|
|
496
|
-
)),
|
|
497
|
-
const SizedBox(width: 16),
|
|
498
|
-
Expanded(
|
|
499
|
-
child: SalaryForm(
|
|
500
|
-
formKey: _salaryFormKey,
|
|
501
|
-
onTransactionSuccess: _fetchData,
|
|
502
|
-
token: token,
|
|
503
|
-
)),
|
|
504
|
-
],
|
|
505
|
-
),
|
|
506
|
-
],
|
|
507
|
-
),
|
|
508
|
-
),
|
|
509
|
-
floatingActionButton: FloatingActionButton(
|
|
510
|
-
onPressed: _fetchData,
|
|
511
|
-
tooltip: 'Refresh Data',
|
|
512
|
-
child: const Icon(Icons.refresh),
|
|
513
|
-
),
|
|
514
|
-
);
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
Widget _buildCashBalanceCard(double totalUserCash, double appCalculatedCash) {
|
|
518
|
-
final double difference = totalUserCash - appCalculatedCash;
|
|
410
|
+
Widget _buildCashBalanceCard(
|
|
411
|
+
BuildContext context, DashboardViewModel viewModel) {
|
|
412
|
+
final double difference =
|
|
413
|
+
viewModel.totalUserCash - viewModel.appCalculatedCash;
|
|
519
414
|
|
|
520
415
|
String differenceLabel = 'Balance is correct';
|
|
521
416
|
Color differenceColor = Theme.of(context).colorScheme.secondary;
|
|
@@ -523,11 +418,12 @@ class _HomeScreenState extends State<HomeScreen> {
|
|
|
523
418
|
|
|
524
419
|
if (difference > 0.01) {
|
|
525
420
|
differenceLabel = 'Missing Income:';
|
|
526
|
-
|
|
421
|
+
// Use semantic color token instead of hardcoded Colors.green
|
|
422
|
+
differenceColor = AppColors.success;
|
|
527
423
|
differenceText = '+${_formatCurrency(difference)}';
|
|
528
424
|
} else if (difference < -0.01) {
|
|
529
425
|
differenceLabel = 'Missing Expenses:';
|
|
530
|
-
differenceColor =
|
|
426
|
+
differenceColor = AppColors.error;
|
|
531
427
|
}
|
|
532
428
|
|
|
533
429
|
return AppFormCard(
|
|
@@ -535,17 +431,17 @@ class _HomeScreenState extends State<HomeScreen> {
|
|
|
535
431
|
child: Column(
|
|
536
432
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
537
433
|
children: [
|
|
538
|
-
_buildBalanceTextField(
|
|
434
|
+
_buildBalanceTextField(viewModel.rbcCashController, 'RBC Bank Balance'),
|
|
539
435
|
const SizedBox(height: 12),
|
|
540
436
|
_buildBalanceTextField(
|
|
541
|
-
|
|
437
|
+
viewModel.wealthsimpleCashController, 'Wealthsimple Balance'),
|
|
542
438
|
const SizedBox(height: 16),
|
|
543
439
|
const Divider(),
|
|
544
440
|
const SizedBox(height: 10),
|
|
545
|
-
_buildResultRow(
|
|
546
|
-
'
|
|
547
|
-
_buildResultRow(
|
|
548
|
-
'
|
|
441
|
+
_buildResultRow('Manual Entry Balance:',
|
|
442
|
+
'${_formatCurrency(viewModel.totalUserCash)}'),
|
|
443
|
+
_buildResultRow('Calculated Balance:',
|
|
444
|
+
'${_formatCurrency(viewModel.appCalculatedCash)}'),
|
|
549
445
|
const SizedBox(height: 10),
|
|
550
446
|
_buildResultRow(differenceLabel, differenceText,
|
|
551
447
|
isHighlighted: true, color: differenceColor),
|
|
@@ -555,7 +451,11 @@ class _HomeScreenState extends State<HomeScreen> {
|
|
|
555
451
|
}
|
|
556
452
|
|
|
557
453
|
Widget _buildCreditCardBalanceCard(
|
|
558
|
-
|
|
454
|
+
BuildContext context,
|
|
455
|
+
DashboardViewModel viewModel,
|
|
456
|
+
String title,
|
|
457
|
+
TextEditingController controller,
|
|
458
|
+
double appExpenses) {
|
|
559
459
|
final double userBalance = CurrencyInputFormatter.unformat(controller.text);
|
|
560
460
|
final double difference = appExpenses - userBalance;
|
|
561
461
|
|
|
@@ -565,11 +465,11 @@ class _HomeScreenState extends State<HomeScreen> {
|
|
|
565
465
|
|
|
566
466
|
if (difference > 0.01) {
|
|
567
467
|
differenceLabel = 'Missing Refund:';
|
|
568
|
-
differenceColor =
|
|
468
|
+
differenceColor = AppColors.success;
|
|
569
469
|
differenceText = '+${_formatCurrency(difference)}';
|
|
570
470
|
} else if (difference < -0.01) {
|
|
571
471
|
differenceLabel = 'Missing Expenses:';
|
|
572
|
-
differenceColor =
|
|
472
|
+
differenceColor = AppColors.error;
|
|
573
473
|
}
|
|
574
474
|
|
|
575
475
|
return AppFormCard(
|
|
@@ -605,7 +505,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
|
|
605
505
|
builder: (context) =>
|
|
606
506
|
ReconciliationScreen(paymentMethod: paymentMethod),
|
|
607
507
|
),
|
|
608
|
-
).then((_) =>
|
|
508
|
+
).then((_) =>
|
|
509
|
+
viewModel.fetchData()); // Refresh data when returning
|
|
609
510
|
},
|
|
610
511
|
child: const Text('Reconcile'),
|
|
611
512
|
),
|
|
@@ -627,7 +528,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
|
|
627
528
|
),
|
|
628
529
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
629
530
|
inputFormatters: [CurrencyInputFormatter()],
|
|
630
|
-
onChanged: (value) => setState(() {}),
|
|
631
531
|
);
|
|
632
532
|
}
|
|
633
533
|
|
|
@@ -659,13 +559,14 @@ class _HomeScreenState extends State<HomeScreen> {
|
|
|
659
559
|
children: transactions.map((transaction) {
|
|
660
560
|
final date = DateFormat('MM/dd')
|
|
661
561
|
.format(DateTime.parse(transaction['date']));
|
|
662
|
-
final amount =
|
|
663
|
-
.
|
|
562
|
+
final amount =
|
|
563
|
+
(double.tryParse(transaction['amount']?.toString() ?? '0.0') ?? 0.0)
|
|
564
|
+
.toStringAsFixed(2);
|
|
664
565
|
final subcategory =
|
|
665
566
|
transaction['subcategory'] ?? 'No subcategory';
|
|
666
567
|
final type = transaction['type'] == 'debit' ? '-' : '+';
|
|
667
568
|
final color =
|
|
668
|
-
transaction['type'] == 'debit' ?
|
|
569
|
+
transaction['type'] == 'debit' ? AppColors.error : AppColors.success;
|
|
669
570
|
|
|
670
571
|
return Padding(
|
|
671
572
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
|
@@ -12,6 +12,11 @@ class DashboardViewModel extends ChangeNotifier {
|
|
|
12
12
|
final rbcCardController = TextEditingController();
|
|
13
13
|
final bmoCardController = TextEditingController();
|
|
14
14
|
|
|
15
|
+
// Form Keys
|
|
16
|
+
final expenseFormKey = GlobalKey<FormState>();
|
|
17
|
+
final investmentFormKey = GlobalKey<FormState>();
|
|
18
|
+
final salaryFormKey = GlobalKey<FormState>();
|
|
19
|
+
|
|
15
20
|
// State variables
|
|
16
21
|
bool _isLoading = true;
|
|
17
22
|
String? _selectedMenu;
|
package/package.json
CHANGED
package/pubspec.yaml
CHANGED
|
@@ -23,6 +23,8 @@ dependencies:
|
|
|
23
23
|
path: ../personal-finance-frontend-feature-investments
|
|
24
24
|
personal_finance_frontend_feature_management:
|
|
25
25
|
path: ../personal-finance-frontend-feature-management
|
|
26
|
+
personal_finance_frontend_feature_profile:
|
|
27
|
+
path: ../personal-finance-frontend-feature-profile
|
|
26
28
|
|
|
27
29
|
dev_dependencies:
|
|
28
30
|
flutter_test:
|