@marcos_feitoza/personal-finance-frontend-feature-dashboard 1.0.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/.circleci/config.yml +23 -0
- package/.metadata +10 -0
- package/CHANGELOG.md +19 -0
- package/LICENSE +1 -0
- package/README.md +39 -0
- package/analysis_options.yaml +4 -0
- package/lib/personal_finance_frontend_feature_dashboard.dart +5 -0
- package/lib/screens/home_screen.dart +694 -0
- package/package.json +29 -0
- package/pubspec.yaml +67 -0
- package/test/personal_finance_frontend_feature_dashboard_test.dart +12 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
version: 2.1
|
|
2
|
+
|
|
3
|
+
jobs:
|
|
4
|
+
release:
|
|
5
|
+
docker:
|
|
6
|
+
- image: circleci/node:latest
|
|
7
|
+
steps:
|
|
8
|
+
- checkout
|
|
9
|
+
- run:
|
|
10
|
+
name: Install dependencies
|
|
11
|
+
command: npm install
|
|
12
|
+
- run:
|
|
13
|
+
name: Run semantic-release
|
|
14
|
+
command: npx semantic-release
|
|
15
|
+
|
|
16
|
+
workflows:
|
|
17
|
+
build-and-release:
|
|
18
|
+
jobs:
|
|
19
|
+
- release:
|
|
20
|
+
filters:
|
|
21
|
+
branches:
|
|
22
|
+
only:
|
|
23
|
+
- main
|
package/.metadata
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# This file tracks properties of this Flutter project.
|
|
2
|
+
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
|
3
|
+
#
|
|
4
|
+
# This file should be version controlled and should not be manually edited.
|
|
5
|
+
|
|
6
|
+
version:
|
|
7
|
+
revision: "fcf2c11572af6f390246c056bc905eca609533a0"
|
|
8
|
+
channel: "stable"
|
|
9
|
+
|
|
10
|
+
project_type: package
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# 1.0.0 (2025-11-22)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* add currency class ([99d9217](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/commit/99d9217d571d15e709b1a16d3845d3e72a244683))
|
|
7
|
+
* add date logs ([45e0a40](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/commit/45e0a40ac9dd690b902e6c345e8bbec9247c8b09))
|
|
8
|
+
* add ignore and more estilo ([ad1c97a](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/commit/ad1c97a4cc267788d92a0912696d0dba3240609a))
|
|
9
|
+
* add new classes ([973d2c7](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/commit/973d2c71a0798ba1020c6456267e37dcbad936a7))
|
|
10
|
+
* dark mode funcionando ([f4d9417](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/commit/f4d9417ee9ad686273397a8bd925fb81407b82ef))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* add avatar ([a1b720a](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/commit/a1b720aa0cd8fd3a70ffcfdac7e7049066416c8f))
|
|
16
|
+
|
|
17
|
+
## 0.0.1
|
|
18
|
+
|
|
19
|
+
* TODO: Describe initial release.
|
package/LICENSE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
TODO: Add your license here.
|
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
This README describes the package. If you publish this package to pub.dev,
|
|
3
|
+
this README's contents appear on the landing page for your package.
|
|
4
|
+
|
|
5
|
+
For information about how to write a good package README, see the guide for
|
|
6
|
+
[writing package pages](https://dart.dev/tools/pub/writing-package-pages).
|
|
7
|
+
|
|
8
|
+
For general information about developing packages, see the Dart guide for
|
|
9
|
+
[creating packages](https://dart.dev/guides/libraries/create-packages)
|
|
10
|
+
and the Flutter guide for
|
|
11
|
+
[developing packages and plugins](https://flutter.dev/to/develop-packages).
|
|
12
|
+
-->
|
|
13
|
+
|
|
14
|
+
TODO: Put a short description of the package here that helps potential users
|
|
15
|
+
know whether this package might be useful for them.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
TODO: List what your package can do. Maybe include images, gifs, or videos.
|
|
20
|
+
|
|
21
|
+
## Getting started
|
|
22
|
+
|
|
23
|
+
TODO: List prerequisites and provide or point to information on how to
|
|
24
|
+
start using the package.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
TODO: Include short and useful examples for package users. Add longer examples
|
|
29
|
+
to `/example` folder.
|
|
30
|
+
|
|
31
|
+
```dart
|
|
32
|
+
const like = 'sample';
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Additional information
|
|
36
|
+
|
|
37
|
+
TODO: Tell users more about the package: where to find more information, how to
|
|
38
|
+
contribute to the package, how to file issues, what response they can expect
|
|
39
|
+
from the package authors, and more.
|
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:flutter/services.dart';
|
|
3
|
+
import 'package:personal_finance_frontend_core_ui/widgets/salary_form.dart';
|
|
4
|
+
import 'package:personal_finance_frontend_core_ui/widgets/expense_form.dart';
|
|
5
|
+
import 'package:personal_finance_frontend_core_ui/widgets/investment_form.dart';
|
|
6
|
+
import 'package:personal_finance_frontend_feature_reports/screens/salary_report_screen.dart';
|
|
7
|
+
import 'package:personal_finance_frontend_feature_charts/screens/salary_chart_screen.dart';
|
|
8
|
+
import 'package:provider/provider.dart';
|
|
9
|
+
import 'package:personal_finance_frontend_feature_reports/screens/expense_report_screen.dart';
|
|
10
|
+
import 'package:personal_finance_frontend_feature_charts/screens/expense_chart_screen.dart';
|
|
11
|
+
import 'package:personal_finance_frontend_feature_reports/screens/summary_report_screen.dart';
|
|
12
|
+
import 'package:personal_finance_frontend_feature_management/screens/manage_transactions_screen.dart';
|
|
13
|
+
import 'package:personal_finance_frontend_feature_reports/screens/move_money_report_screen.dart';
|
|
14
|
+
import 'package:personal_finance_frontend_feature_investments/screens/investment_account_screen.dart';
|
|
15
|
+
import 'package:personal_finance_frontend_feature_investments/screens/rrsp_sun_life_screen.dart';
|
|
16
|
+
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
|
+
import 'package:intl/intl.dart';
|
|
23
|
+
import 'package:personal_finance_frontend_feature_management/screens/reconciliation_screen.dart';
|
|
24
|
+
import 'package:personal_finance_frontend_core_ui/widgets/app_widgets.dart';
|
|
25
|
+
import 'package:personal_finance_frontend_core_ui/utils/theme_notifier.dart';
|
|
26
|
+
import 'package:personal_finance_frontend_core_services/providers/auth_provider.dart';
|
|
27
|
+
import 'package:personal_finance_frontend_core_ui/widgets/user_profile_avatar.dart';
|
|
28
|
+
|
|
29
|
+
class HomeScreen extends StatefulWidget {
|
|
30
|
+
const HomeScreen({Key? key}) : super(key: key);
|
|
31
|
+
|
|
32
|
+
@override
|
|
33
|
+
_HomeScreenState createState() => _HomeScreenState();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
final String currencySymbol = r'$';
|
|
37
|
+
|
|
38
|
+
String _formatCurrency(double value) {
|
|
39
|
+
final format = NumberFormat.currency(
|
|
40
|
+
symbol: currencySymbol,
|
|
41
|
+
decimalDigits: 2,
|
|
42
|
+
locale: 'en_US', // garante vírgula de milhar e ponto decimal no padrão US
|
|
43
|
+
);
|
|
44
|
+
return format.format(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class _HomeScreenState extends State<HomeScreen> {
|
|
48
|
+
final _salaryFormKey = GlobalKey<FormState>();
|
|
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
|
+
}
|
|
85
|
+
|
|
86
|
+
@override
|
|
87
|
+
void dispose() {
|
|
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;
|
|
97
|
+
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
|
98
|
+
final token = authProvider.token;
|
|
99
|
+
|
|
100
|
+
setState(() {
|
|
101
|
+
_isLoading = true;
|
|
102
|
+
});
|
|
103
|
+
try {
|
|
104
|
+
// Fetch all balances, including the definitive cash balance from the backend
|
|
105
|
+
final cashBalanceFuture =
|
|
106
|
+
_transactionService.getAccountBalance('CASH', token: token);
|
|
107
|
+
final investmentBalanceFutures = _investmentAccountNames
|
|
108
|
+
.map((name) =>
|
|
109
|
+
_transactionService.getAccountBalance(name, token: token))
|
|
110
|
+
.toList();
|
|
111
|
+
|
|
112
|
+
final otherFutures = <Future<dynamic>>[
|
|
113
|
+
_transactionService.getSummaryByPaymentMethod(token: token),
|
|
114
|
+
_transactionService.fetchTransactions(
|
|
115
|
+
paymentMethod: 'CASH', limit: 10, token: token),
|
|
116
|
+
_transactionService.fetchTransactions(
|
|
117
|
+
paymentMethod: 'RBC', limit: 10, token: token),
|
|
118
|
+
_transactionService.fetchTransactions(
|
|
119
|
+
paymentMethod: 'BMO', limit: 10, token: token),
|
|
120
|
+
_transactionService.getInvestments(token: token),
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
// Await all futures
|
|
124
|
+
final allInvestmentBalances = await Future.wait(investmentBalanceFutures);
|
|
125
|
+
final cashBalance = await cashBalanceFuture;
|
|
126
|
+
final otherResults = await Future.wait(otherFutures);
|
|
127
|
+
|
|
128
|
+
// Safely build the account balances map
|
|
129
|
+
_accountBalances = {'CASH': cashBalance};
|
|
130
|
+
for (int i = 0; i < _investmentAccountNames.length; i++) {
|
|
131
|
+
_accountBalances[_investmentAccountNames[i]] = allInvestmentBalances[i];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
final pmSummary = otherResults[0] as List<Map<String, dynamic>>;
|
|
135
|
+
_rbcUnpaidExpenses = 0.0;
|
|
136
|
+
_bmoUnpaidExpenses = 0.0;
|
|
137
|
+
for (var item in pmSummary) {
|
|
138
|
+
final paymentMethod =
|
|
139
|
+
(item['payment_method'] as String?)?.toUpperCase();
|
|
140
|
+
if (paymentMethod == 'RBC') {
|
|
141
|
+
_rbcUnpaidExpenses += double.parse(item['unpaid_amount'].toString());
|
|
142
|
+
} else if (paymentMethod == 'BMO') {
|
|
143
|
+
_bmoUnpaidExpenses += double.parse(item['unpaid_amount'].toString());
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
_cashTransactions = otherResults[1] as List<Map<String, dynamic>>;
|
|
148
|
+
_rbcTransactions = otherResults[2] as List<Map<String, dynamic>>;
|
|
149
|
+
_bmoTransactions = otherResults[3] as List<Map<String, dynamic>>;
|
|
150
|
+
_investments = otherResults[4] as List<Map<String, dynamic>>;
|
|
151
|
+
} catch (e) {
|
|
152
|
+
print('Error fetching data: $e');
|
|
153
|
+
} finally {
|
|
154
|
+
if (mounted) {
|
|
155
|
+
setState(() {
|
|
156
|
+
_isLoading = false;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
Widget _buildMainMenu(BuildContext context) {
|
|
163
|
+
return ListView(
|
|
164
|
+
padding: EdgeInsets.zero,
|
|
165
|
+
children: <Widget>[
|
|
166
|
+
const DrawerHeader(
|
|
167
|
+
decoration: BoxDecoration(
|
|
168
|
+
color: Colors.blue,
|
|
169
|
+
),
|
|
170
|
+
child: Text(
|
|
171
|
+
'Menu',
|
|
172
|
+
style: TextStyle(
|
|
173
|
+
color: Colors.white,
|
|
174
|
+
fontSize: 24,
|
|
175
|
+
),
|
|
176
|
+
),
|
|
177
|
+
),
|
|
178
|
+
ListTile(
|
|
179
|
+
leading: const Icon(Icons.assessment),
|
|
180
|
+
title: const Text('Investments'),
|
|
181
|
+
onTap: () {
|
|
182
|
+
setState(() {
|
|
183
|
+
_selectedMenu = 'investments';
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
),
|
|
187
|
+
ListTile(
|
|
188
|
+
leading: const Icon(Icons.description),
|
|
189
|
+
title: const Text('Reports'),
|
|
190
|
+
onTap: () {
|
|
191
|
+
setState(() {
|
|
192
|
+
_selectedMenu = 'reports';
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
),
|
|
196
|
+
ListTile(
|
|
197
|
+
leading: const Icon(Icons.bar_chart),
|
|
198
|
+
title: const Text('Charts'),
|
|
199
|
+
onTap: () {
|
|
200
|
+
setState(() {
|
|
201
|
+
_selectedMenu = 'charts';
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
),
|
|
205
|
+
ListTile(
|
|
206
|
+
leading: const Icon(Icons.settings),
|
|
207
|
+
title: const Text('Management'),
|
|
208
|
+
onTap: () {
|
|
209
|
+
setState(() {
|
|
210
|
+
_selectedMenu = 'management';
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
),
|
|
214
|
+
],
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
Widget _buildSubMenu(BuildContext context) {
|
|
219
|
+
List<Widget> items;
|
|
220
|
+
String title;
|
|
221
|
+
|
|
222
|
+
switch (_selectedMenu) {
|
|
223
|
+
case 'investments':
|
|
224
|
+
title = 'Investments';
|
|
225
|
+
items = [
|
|
226
|
+
..._investmentAccountNames.map((accountName) {
|
|
227
|
+
final balance = _accountBalances[accountName] ?? 0.0;
|
|
228
|
+
final formattedBalance = _formatCurrency(balance);
|
|
229
|
+
return ListTile(
|
|
230
|
+
title: Text(' $accountName - $formattedBalance'),
|
|
231
|
+
onTap: () {
|
|
232
|
+
Navigator.pop(context); // Close drawer
|
|
233
|
+
if (accountName == 'Crypto') {
|
|
234
|
+
Navigator.push(
|
|
235
|
+
context,
|
|
236
|
+
MaterialPageRoute(
|
|
237
|
+
builder: (context) =>
|
|
238
|
+
CryptoAccountScreen(accountName: accountName),
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
} else {
|
|
242
|
+
Navigator.push(
|
|
243
|
+
context,
|
|
244
|
+
MaterialPageRoute(
|
|
245
|
+
builder: (context) => InvestmentAccountScreen(
|
|
246
|
+
accountName: accountName,
|
|
247
|
+
showDividends: accountName != 'Crypto',
|
|
248
|
+
),
|
|
249
|
+
),
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
);
|
|
254
|
+
}).toList(),
|
|
255
|
+
ListTile(
|
|
256
|
+
title: const Text(' RRSP Sun Life'),
|
|
257
|
+
onTap: () {
|
|
258
|
+
Navigator.pop(context); // Close drawer
|
|
259
|
+
Navigator.push(
|
|
260
|
+
context,
|
|
261
|
+
MaterialPageRoute(
|
|
262
|
+
builder: (context) => const RrspSunLifeScreen()),
|
|
263
|
+
);
|
|
264
|
+
},
|
|
265
|
+
),
|
|
266
|
+
];
|
|
267
|
+
break;
|
|
268
|
+
case 'reports':
|
|
269
|
+
title = 'Reports';
|
|
270
|
+
items = [
|
|
271
|
+
ListTile(
|
|
272
|
+
title: const Text(' Salary Report'),
|
|
273
|
+
onTap: () {
|
|
274
|
+
Navigator.pop(context);
|
|
275
|
+
Navigator.push(
|
|
276
|
+
context,
|
|
277
|
+
MaterialPageRoute(
|
|
278
|
+
builder: (context) => const SalaryReportScreen()),
|
|
279
|
+
);
|
|
280
|
+
},
|
|
281
|
+
),
|
|
282
|
+
ListTile(
|
|
283
|
+
title: const Text(' Expense Report'),
|
|
284
|
+
onTap: () {
|
|
285
|
+
Navigator.pop(context);
|
|
286
|
+
Navigator.push(
|
|
287
|
+
context,
|
|
288
|
+
MaterialPageRoute(
|
|
289
|
+
builder: (context) => const ExpenseReportScreen()),
|
|
290
|
+
);
|
|
291
|
+
},
|
|
292
|
+
),
|
|
293
|
+
ListTile(
|
|
294
|
+
title: const Text(' Summary Report'),
|
|
295
|
+
onTap: () {
|
|
296
|
+
Navigator.pop(context);
|
|
297
|
+
Navigator.push(
|
|
298
|
+
context,
|
|
299
|
+
MaterialPageRoute(
|
|
300
|
+
builder: (context) => const SummaryReportScreen()),
|
|
301
|
+
);
|
|
302
|
+
},
|
|
303
|
+
),
|
|
304
|
+
ListTile(
|
|
305
|
+
title: const Text(' Move Money Report'),
|
|
306
|
+
onTap: () {
|
|
307
|
+
Navigator.pop(context);
|
|
308
|
+
Navigator.push(
|
|
309
|
+
context,
|
|
310
|
+
MaterialPageRoute(
|
|
311
|
+
builder: (context) => const MovemoneyReportScreen()));
|
|
312
|
+
},
|
|
313
|
+
),
|
|
314
|
+
];
|
|
315
|
+
break;
|
|
316
|
+
case 'charts':
|
|
317
|
+
title = 'Charts';
|
|
318
|
+
items = [
|
|
319
|
+
ListTile(
|
|
320
|
+
title: const Text(' Salary Chart'),
|
|
321
|
+
onTap: () {
|
|
322
|
+
Navigator.pop(context);
|
|
323
|
+
Navigator.push(
|
|
324
|
+
context,
|
|
325
|
+
MaterialPageRoute(
|
|
326
|
+
builder: (context) => const SalaryChartScreen()),
|
|
327
|
+
);
|
|
328
|
+
},
|
|
329
|
+
),
|
|
330
|
+
ListTile(
|
|
331
|
+
title: const Text(' Expense Chart'),
|
|
332
|
+
onTap: () {
|
|
333
|
+
Navigator.pop(context);
|
|
334
|
+
Navigator.push(
|
|
335
|
+
context,
|
|
336
|
+
MaterialPageRoute(
|
|
337
|
+
builder: (context) => const ExpenseChartScreen()),
|
|
338
|
+
);
|
|
339
|
+
},
|
|
340
|
+
),
|
|
341
|
+
];
|
|
342
|
+
break;
|
|
343
|
+
case 'management':
|
|
344
|
+
title = 'Management';
|
|
345
|
+
items = [
|
|
346
|
+
ListTile(
|
|
347
|
+
title: const Text(' Manage Transactions'),
|
|
348
|
+
onTap: () {
|
|
349
|
+
Navigator.pop(context);
|
|
350
|
+
Navigator.push(
|
|
351
|
+
context,
|
|
352
|
+
MaterialPageRoute(
|
|
353
|
+
builder: (context) => const ManageTransactionsScreen()),
|
|
354
|
+
).then((_) => _fetchData());
|
|
355
|
+
},
|
|
356
|
+
),
|
|
357
|
+
ListTile(
|
|
358
|
+
title: const Text(' Reconciliation'),
|
|
359
|
+
onTap: () {
|
|
360
|
+
Navigator.pop(context);
|
|
361
|
+
Navigator.push(
|
|
362
|
+
context,
|
|
363
|
+
MaterialPageRoute(
|
|
364
|
+
builder: (context) => const ReconciliationScreen(
|
|
365
|
+
paymentMethod: '')), // Pass empty string for now
|
|
366
|
+
);
|
|
367
|
+
},
|
|
368
|
+
),
|
|
369
|
+
];
|
|
370
|
+
break;
|
|
371
|
+
default:
|
|
372
|
+
items = [];
|
|
373
|
+
title = 'Menu';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return ListView(
|
|
377
|
+
padding: EdgeInsets.zero,
|
|
378
|
+
children: <Widget>[
|
|
379
|
+
AppBar(
|
|
380
|
+
title: Text(title),
|
|
381
|
+
backgroundColor: Theme.of(context).primaryColor,
|
|
382
|
+
leading: IconButton(
|
|
383
|
+
icon: const Icon(Icons.arrow_back),
|
|
384
|
+
onPressed: () {
|
|
385
|
+
setState(() {
|
|
386
|
+
_selectedMenu = null;
|
|
387
|
+
});
|
|
388
|
+
},
|
|
389
|
+
),
|
|
390
|
+
automaticallyImplyLeading: false,
|
|
391
|
+
),
|
|
392
|
+
...items,
|
|
393
|
+
],
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
@override
|
|
398
|
+
Widget build(BuildContext context) {
|
|
399
|
+
final double totalUserCash =
|
|
400
|
+
CurrencyInputFormatter.unformat(_rbcCashController.text) +
|
|
401
|
+
CurrencyInputFormatter.unformat(_wealthsimpleCashController.text);
|
|
402
|
+
|
|
403
|
+
final double appCalculatedCash = _accountBalances['CASH'] ?? 0.0;
|
|
404
|
+
final token = Provider.of<AuthProvider>(context, listen: false).token;
|
|
405
|
+
|
|
406
|
+
return Scaffold(
|
|
407
|
+
appBar: AppBar(
|
|
408
|
+
title: const Text('Personal Finance Dashboard'),
|
|
409
|
+
actions: [
|
|
410
|
+
const UserProfileAvatar(),
|
|
411
|
+
Consumer<ThemeNotifier>(
|
|
412
|
+
builder: (context, themeNotifier, child) {
|
|
413
|
+
return IconButton(
|
|
414
|
+
icon: Icon(themeNotifier.themeMode == ThemeMode.dark
|
|
415
|
+
? Icons.dark_mode
|
|
416
|
+
: Icons.light_mode),
|
|
417
|
+
onPressed: () {
|
|
418
|
+
themeNotifier.setThemeMode(
|
|
419
|
+
themeNotifier.themeMode == ThemeMode.dark
|
|
420
|
+
? ThemeMode.light
|
|
421
|
+
: ThemeMode.dark);
|
|
422
|
+
},
|
|
423
|
+
);
|
|
424
|
+
},
|
|
425
|
+
),
|
|
426
|
+
IconButton(
|
|
427
|
+
icon: const Icon(Icons.logout),
|
|
428
|
+
onPressed: () {
|
|
429
|
+
Provider.of<AuthProvider>(context, listen: false).logout();
|
|
430
|
+
},
|
|
431
|
+
),
|
|
432
|
+
],
|
|
433
|
+
),
|
|
434
|
+
drawer: Drawer(
|
|
435
|
+
child: _selectedMenu == null
|
|
436
|
+
? _buildMainMenu(context)
|
|
437
|
+
: _buildSubMenu(context),
|
|
438
|
+
),
|
|
439
|
+
body: _isLoading
|
|
440
|
+
? const Center(child: CircularProgressIndicator())
|
|
441
|
+
: SingleChildScrollView(
|
|
442
|
+
padding: const EdgeInsets.all(16.0),
|
|
443
|
+
child: Column(
|
|
444
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
445
|
+
children: [
|
|
446
|
+
Row(
|
|
447
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
448
|
+
children: [
|
|
449
|
+
Expanded(
|
|
450
|
+
child: _buildCashBalanceCard(
|
|
451
|
+
totalUserCash, appCalculatedCash)),
|
|
452
|
+
const SizedBox(width: 16),
|
|
453
|
+
Expanded(
|
|
454
|
+
child: _buildCreditCardBalanceCard('RBC Balance',
|
|
455
|
+
_rbcCardController, _rbcUnpaidExpenses)),
|
|
456
|
+
const SizedBox(width: 16),
|
|
457
|
+
Expanded(
|
|
458
|
+
child: _buildCreditCardBalanceCard('BMO Balance',
|
|
459
|
+
_bmoCardController, _bmoUnpaidExpenses)),
|
|
460
|
+
],
|
|
461
|
+
),
|
|
462
|
+
const SizedBox(height: 16),
|
|
463
|
+
Row(
|
|
464
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
465
|
+
children: [
|
|
466
|
+
Expanded(
|
|
467
|
+
child: _buildRecentTransactionsList(
|
|
468
|
+
'Cash Transactions', _cashTransactions)),
|
|
469
|
+
const SizedBox(width: 16),
|
|
470
|
+
Expanded(
|
|
471
|
+
child: _buildRecentTransactionsList(
|
|
472
|
+
'RBC Transactions', _rbcTransactions)),
|
|
473
|
+
const SizedBox(width: 16),
|
|
474
|
+
Expanded(
|
|
475
|
+
child: _buildRecentTransactionsList(
|
|
476
|
+
'BMO Transactions', _bmoTransactions)),
|
|
477
|
+
const SizedBox(width: 16),
|
|
478
|
+
Expanded(
|
|
479
|
+
child: ExpenseForm(
|
|
480
|
+
formKey: _expenseFormKey,
|
|
481
|
+
onTransactionSuccess: _fetchData,
|
|
482
|
+
token: token,
|
|
483
|
+
)),
|
|
484
|
+
],
|
|
485
|
+
),
|
|
486
|
+
const SizedBox(height: 16),
|
|
487
|
+
Row(
|
|
488
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
489
|
+
children: [
|
|
490
|
+
Expanded(
|
|
491
|
+
child: InvestmentForm(
|
|
492
|
+
formKey: _investmentFormKey,
|
|
493
|
+
accountBalances: _accountBalances,
|
|
494
|
+
cashBalance: appCalculatedCash,
|
|
495
|
+
onTransactionSuccess: _fetchData,
|
|
496
|
+
token: token,
|
|
497
|
+
)),
|
|
498
|
+
const SizedBox(width: 16),
|
|
499
|
+
Expanded(
|
|
500
|
+
child: SalaryForm(
|
|
501
|
+
formKey: _salaryFormKey,
|
|
502
|
+
onTransactionSuccess: _fetchData,
|
|
503
|
+
token: token,
|
|
504
|
+
)),
|
|
505
|
+
],
|
|
506
|
+
),
|
|
507
|
+
],
|
|
508
|
+
),
|
|
509
|
+
),
|
|
510
|
+
floatingActionButton: FloatingActionButton(
|
|
511
|
+
onPressed: _fetchData,
|
|
512
|
+
tooltip: 'Refresh Data',
|
|
513
|
+
child: const Icon(Icons.refresh),
|
|
514
|
+
),
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
Widget _buildCashBalanceCard(double totalUserCash, double appCalculatedCash) {
|
|
519
|
+
final double difference = totalUserCash - appCalculatedCash;
|
|
520
|
+
|
|
521
|
+
String differenceLabel = 'Balance is correct';
|
|
522
|
+
Color differenceColor = Theme.of(context).colorScheme.secondary;
|
|
523
|
+
String differenceText = _formatCurrency(difference);
|
|
524
|
+
|
|
525
|
+
if (difference > 0.01) {
|
|
526
|
+
differenceLabel = 'Missing Income:';
|
|
527
|
+
differenceColor = Colors.green;
|
|
528
|
+
differenceText = '+${_formatCurrency(difference)}';
|
|
529
|
+
} else if (difference < -0.01) {
|
|
530
|
+
differenceLabel = 'Missing Expenses:';
|
|
531
|
+
differenceColor = Colors.red;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return AppFormCard(
|
|
535
|
+
title: 'Cash Balance',
|
|
536
|
+
child: Column(
|
|
537
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
538
|
+
children: [
|
|
539
|
+
_buildBalanceTextField(_rbcCashController, 'RBC Bank Balance'),
|
|
540
|
+
const SizedBox(height: 12),
|
|
541
|
+
_buildBalanceTextField(
|
|
542
|
+
_wealthsimpleCashController, 'Wealthsimple Balance'),
|
|
543
|
+
const SizedBox(height: 16),
|
|
544
|
+
const Divider(),
|
|
545
|
+
const SizedBox(height: 10),
|
|
546
|
+
_buildResultRow(
|
|
547
|
+
'Manual Entry Balance:', '${_formatCurrency(totalUserCash)}'),
|
|
548
|
+
_buildResultRow(
|
|
549
|
+
'Calculated Balance:', '${_formatCurrency(appCalculatedCash)}'),
|
|
550
|
+
const SizedBox(height: 10),
|
|
551
|
+
_buildResultRow(differenceLabel, differenceText,
|
|
552
|
+
isHighlighted: true, color: differenceColor),
|
|
553
|
+
],
|
|
554
|
+
),
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
Widget _buildCreditCardBalanceCard(
|
|
559
|
+
String title, TextEditingController controller, double appExpenses) {
|
|
560
|
+
final double userBalance = CurrencyInputFormatter.unformat(controller.text);
|
|
561
|
+
final double difference = appExpenses - userBalance;
|
|
562
|
+
|
|
563
|
+
String differenceLabel = 'Balance is correct';
|
|
564
|
+
Color differenceColor = Theme.of(context).colorScheme.secondary;
|
|
565
|
+
String differenceText = '${_formatCurrency(difference)}';
|
|
566
|
+
|
|
567
|
+
if (difference > 0.01) {
|
|
568
|
+
differenceLabel = 'Missing Expenses:';
|
|
569
|
+
differenceColor = Colors.red;
|
|
570
|
+
differenceText = '+${_formatCurrency(difference)}';
|
|
571
|
+
} else if (difference < -0.01) {
|
|
572
|
+
differenceLabel = 'Missing Refund:';
|
|
573
|
+
differenceColor = Colors.green;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return AppFormCard(
|
|
577
|
+
title: title,
|
|
578
|
+
child: Column(
|
|
579
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
580
|
+
children: [
|
|
581
|
+
_buildBalanceTextField(controller, 'Statement Balance:'),
|
|
582
|
+
const SizedBox(height: 16),
|
|
583
|
+
const Divider(),
|
|
584
|
+
const SizedBox(height: 10),
|
|
585
|
+
_buildResultRow(
|
|
586
|
+
'Manual Entry Balance:', '${_formatCurrency(userBalance)}'),
|
|
587
|
+
_buildResultRow(
|
|
588
|
+
'Calculated Balance:', '${_formatCurrency(appExpenses)}'),
|
|
589
|
+
const SizedBox(height: 10),
|
|
590
|
+
_buildResultRow(differenceLabel, differenceText,
|
|
591
|
+
isHighlighted: true, color: differenceColor),
|
|
592
|
+
const SizedBox(height: 16),
|
|
593
|
+
Align(
|
|
594
|
+
alignment: Alignment.centerRight,
|
|
595
|
+
child: TextButton(
|
|
596
|
+
onPressed: () {
|
|
597
|
+
String paymentMethod = 'unknown';
|
|
598
|
+
if (title.toLowerCase().contains('rbc')) {
|
|
599
|
+
paymentMethod = 'rbc';
|
|
600
|
+
} else if (title.toLowerCase().contains('bmo')) {
|
|
601
|
+
paymentMethod = 'bmo';
|
|
602
|
+
}
|
|
603
|
+
Navigator.push(
|
|
604
|
+
context,
|
|
605
|
+
MaterialPageRoute(
|
|
606
|
+
builder: (context) =>
|
|
607
|
+
ReconciliationScreen(paymentMethod: paymentMethod),
|
|
608
|
+
),
|
|
609
|
+
).then((_) => _fetchData()); // Refresh data when returning
|
|
610
|
+
},
|
|
611
|
+
child: const Text('Reconcile'),
|
|
612
|
+
),
|
|
613
|
+
),
|
|
614
|
+
],
|
|
615
|
+
),
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
Widget _buildBalanceTextField(
|
|
620
|
+
TextEditingController controller, String label) {
|
|
621
|
+
return TextFormField(
|
|
622
|
+
controller: controller,
|
|
623
|
+
decoration: InputDecoration(
|
|
624
|
+
labelText: label,
|
|
625
|
+
border: OutlineInputBorder(
|
|
626
|
+
borderRadius: BorderRadius.circular(16.0),
|
|
627
|
+
),
|
|
628
|
+
),
|
|
629
|
+
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
630
|
+
inputFormatters: [CurrencyInputFormatter()],
|
|
631
|
+
onChanged: (value) => setState(() {}),
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
Widget _buildResultRow(String label, String value,
|
|
636
|
+
{bool isHighlighted = false, Color color = Colors.black}) {
|
|
637
|
+
return Row(
|
|
638
|
+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
639
|
+
children: [
|
|
640
|
+
Text(label, style: const TextStyle(fontSize: 16)),
|
|
641
|
+
Text(
|
|
642
|
+
value,
|
|
643
|
+
style: TextStyle(
|
|
644
|
+
fontSize: 16,
|
|
645
|
+
fontWeight: isHighlighted ? FontWeight.bold : FontWeight.normal,
|
|
646
|
+
color: isHighlighted ? color : null,
|
|
647
|
+
),
|
|
648
|
+
),
|
|
649
|
+
],
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
Widget _buildRecentTransactionsList(
|
|
654
|
+
String title, List<Map<String, dynamic>> transactions) {
|
|
655
|
+
return AppFormCard(
|
|
656
|
+
title: title,
|
|
657
|
+
child: transactions.isEmpty
|
|
658
|
+
? const Text('No recent transactions.')
|
|
659
|
+
: Column(
|
|
660
|
+
children: transactions.map((transaction) {
|
|
661
|
+
final date = DateFormat('MM/dd')
|
|
662
|
+
.format(DateTime.parse(transaction['date']));
|
|
663
|
+
final amount = double.parse(transaction['amount'].toString())
|
|
664
|
+
.toStringAsFixed(2);
|
|
665
|
+
final subcategory =
|
|
666
|
+
transaction['subcategory'] ?? 'No subcategory';
|
|
667
|
+
final type = transaction['type'] == 'debit' ? '-' : '+';
|
|
668
|
+
final color =
|
|
669
|
+
transaction['type'] == 'debit' ? Colors.red : Colors.green;
|
|
670
|
+
|
|
671
|
+
return Padding(
|
|
672
|
+
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
|
673
|
+
child: Row(
|
|
674
|
+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
675
|
+
children: [
|
|
676
|
+
Expanded(
|
|
677
|
+
child: Text(
|
|
678
|
+
'$date - $subcategory',
|
|
679
|
+
overflow: TextOverflow.ellipsis,
|
|
680
|
+
),
|
|
681
|
+
),
|
|
682
|
+
Text(
|
|
683
|
+
'$type\u0024$amount',
|
|
684
|
+
style: TextStyle(
|
|
685
|
+
color: color, fontWeight: FontWeight.bold),
|
|
686
|
+
),
|
|
687
|
+
],
|
|
688
|
+
),
|
|
689
|
+
);
|
|
690
|
+
}).toList(),
|
|
691
|
+
),
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@marcos_feitoza/personal-finance-frontend-feature-dashboard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"scripts": {
|
|
8
|
+
"release": "semantic-release"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"semantic-release": "^18.0.0",
|
|
12
|
+
"@semantic-release/changelog": "^6.0.0",
|
|
13
|
+
"@semantic-release/git": "^10.0.0",
|
|
14
|
+
"@semantic-release/github": "^8.0.0"
|
|
15
|
+
},
|
|
16
|
+
"release": {
|
|
17
|
+
"branches": [
|
|
18
|
+
"main"
|
|
19
|
+
],
|
|
20
|
+
"plugins": [
|
|
21
|
+
"@semantic-release/commit-analyzer",
|
|
22
|
+
"@semantic-release/release-notes-generator",
|
|
23
|
+
"@semantic-release/changelog",
|
|
24
|
+
"@semantic-release/npm",
|
|
25
|
+
"@semantic-release/github",
|
|
26
|
+
"@semantic-release/git"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|
package/pubspec.yaml
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
name: personal_finance_frontend_feature_dashboard
|
|
2
|
+
description: "A new Flutter package project."
|
|
3
|
+
version: 0.0.1
|
|
4
|
+
homepage:
|
|
5
|
+
|
|
6
|
+
environment:
|
|
7
|
+
sdk: '>=2.19.0 <3.0.0'
|
|
8
|
+
|
|
9
|
+
dependencies:
|
|
10
|
+
flutter:
|
|
11
|
+
sdk: flutter
|
|
12
|
+
intl: ^0.17.0
|
|
13
|
+
provider: ^6.0.0
|
|
14
|
+
personal_finance_frontend_core_services:
|
|
15
|
+
path: ../personal-finance-frontend-core-services
|
|
16
|
+
personal_finance_frontend_core_ui:
|
|
17
|
+
path: ../personal-finance-frontend-core-ui
|
|
18
|
+
personal_finance_frontend_feature_reports:
|
|
19
|
+
path: ../personal-finance-frontend-feature-reports
|
|
20
|
+
personal_finance_frontend_feature_charts:
|
|
21
|
+
path: ../personal-finance-frontend-feature-charts
|
|
22
|
+
personal_finance_frontend_feature_investments:
|
|
23
|
+
path: ../personal-finance-frontend-feature-investments
|
|
24
|
+
personal_finance_frontend_feature_management:
|
|
25
|
+
path: ../personal-finance-frontend-feature-management
|
|
26
|
+
|
|
27
|
+
dev_dependencies:
|
|
28
|
+
flutter_test:
|
|
29
|
+
sdk: flutter
|
|
30
|
+
flutter_lints: ^5.0.0
|
|
31
|
+
|
|
32
|
+
# For information on the generic Dart part of this file, see the
|
|
33
|
+
# following page: https://dart.dev/tools/pub/pubspec
|
|
34
|
+
|
|
35
|
+
# The following section is specific to Flutter packages.
|
|
36
|
+
flutter:
|
|
37
|
+
|
|
38
|
+
# To add assets to your package, add an assets section, like this:
|
|
39
|
+
# assets:
|
|
40
|
+
# - images/a_dot_burr.jpeg
|
|
41
|
+
# - images/a_dot_ham.jpeg
|
|
42
|
+
#
|
|
43
|
+
# For details regarding assets in packages, see
|
|
44
|
+
# https://flutter.dev/to/asset-from-package
|
|
45
|
+
#
|
|
46
|
+
# An image asset can refer to one or more resolution-specific "variants", see
|
|
47
|
+
# https://flutter.dev/to/resolution-aware-images
|
|
48
|
+
|
|
49
|
+
# To add custom fonts to your package, add a fonts section here,
|
|
50
|
+
# in this "flutter" section. Each entry in this list should have a
|
|
51
|
+
# "family" key with the font family name, and a "fonts" key with a
|
|
52
|
+
# list giving the asset and other descriptors for the font. For
|
|
53
|
+
# example:
|
|
54
|
+
# fonts:
|
|
55
|
+
# - family: Schyler
|
|
56
|
+
# fonts:
|
|
57
|
+
# - asset: fonts/Schyler-Regular.ttf
|
|
58
|
+
# - asset: fonts/Schyler-Italic.ttf
|
|
59
|
+
# style: italic
|
|
60
|
+
# - family: Trajan Pro
|
|
61
|
+
# fonts:
|
|
62
|
+
# - asset: fonts/TrajanPro.ttf
|
|
63
|
+
# - asset: fonts/TrajanPro_Bold.ttf
|
|
64
|
+
# weight: 700
|
|
65
|
+
#
|
|
66
|
+
# For details regarding fonts in packages, see
|
|
67
|
+
# https://flutter.dev/to/font-from-package
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import 'package:flutter_test/flutter_test.dart';
|
|
2
|
+
|
|
3
|
+
import 'package:personal_finance_frontend_feature_dashboard/personal_finance_frontend_feature_dashboard.dart';
|
|
4
|
+
|
|
5
|
+
void main() {
|
|
6
|
+
test('adds one to input values', () {
|
|
7
|
+
final calculator = Calculator();
|
|
8
|
+
expect(calculator.addOne(2), 3);
|
|
9
|
+
expect(calculator.addOne(-7), -6);
|
|
10
|
+
expect(calculator.addOne(0), 1);
|
|
11
|
+
});
|
|
12
|
+
}
|