@marcos_feitoza/personal-finance-frontend-feature-investments 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 +30 -0
- package/LICENSE +1 -0
- package/README.md +39 -0
- package/analysis_options.yaml +4 -0
- package/lib/personal_finance_frontend_feature_investments.dart +5 -0
- package/lib/screens/crypto_account_screen.dart +381 -0
- package/lib/screens/investment_account_screen.dart +624 -0
- package/lib/screens/rrsp_sun_life_screen.dart +340 -0
- package/package.json +29 -0
- package/pubspec.yaml +59 -0
- package/test/personal_finance_frontend_feature_investments_test.dart +12 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:intl/intl.dart';
|
|
3
|
+
import 'package:personal_finance_frontend_core_services/services/transaction_service.dart';
|
|
4
|
+
import 'package:personal_finance_frontend_core_ui/widgets/rrsp_contribution_form.dart';
|
|
5
|
+
import 'package:provider/provider.dart';
|
|
6
|
+
import 'package:personal_finance_frontend_core_services/providers/auth_provider.dart';
|
|
7
|
+
|
|
8
|
+
class RrspSunLifeScreen extends StatefulWidget {
|
|
9
|
+
const RrspSunLifeScreen({Key? key}) : super(key: key);
|
|
10
|
+
|
|
11
|
+
@override
|
|
12
|
+
_RrspSunLifeScreenState createState() => _RrspSunLifeScreenState();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
16
|
+
final TransactionService _transactionService = TransactionService();
|
|
17
|
+
List<Map<String, dynamic>> _contributions = [];
|
|
18
|
+
List<Map<String, dynamic>> _sinteticoSummary = [];
|
|
19
|
+
double _rrspTotalValue = 0.0;
|
|
20
|
+
double _rrspCashBalance = 0.0;
|
|
21
|
+
double _totalPortfolioBookCost = 0.0;
|
|
22
|
+
bool _isLoading = true;
|
|
23
|
+
String? _token;
|
|
24
|
+
|
|
25
|
+
@override
|
|
26
|
+
void initState() {
|
|
27
|
+
super.initState();
|
|
28
|
+
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
29
|
+
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
|
30
|
+
setState(() {
|
|
31
|
+
_token = authProvider.token;
|
|
32
|
+
});
|
|
33
|
+
_fetchData();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
String currencySymbol = r'$'; // ou poderia vir de configuração, API, etc.
|
|
38
|
+
|
|
39
|
+
String _formatCurrency(double value) {
|
|
40
|
+
return '$currencySymbol${value.toStringAsFixed(2)}';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Future<void> _fetchData() async {
|
|
44
|
+
if (_token == null) return;
|
|
45
|
+
if (mounted) {
|
|
46
|
+
setState(() {
|
|
47
|
+
_isLoading = true;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
final contributions = await _transactionService.getRrspContributions(
|
|
53
|
+
investmentAccount: 'RRSP Sun Life', token: _token);
|
|
54
|
+
final balance =
|
|
55
|
+
await _transactionService.getAccountBalance('RRSP Sun Life', token: _token);
|
|
56
|
+
final totalPortfolioBookCost =
|
|
57
|
+
await _transactionService.getTotalPortfolioBookCost(token: _token);
|
|
58
|
+
|
|
59
|
+
final sinteticoSummaryData =
|
|
60
|
+
_calculateSinteticoSummary(contributions, totalPortfolioBookCost);
|
|
61
|
+
|
|
62
|
+
print('[DEBUG] Sintetico Summary Data: $sinteticoSummaryData');
|
|
63
|
+
|
|
64
|
+
if (!mounted) return;
|
|
65
|
+
setState(() {
|
|
66
|
+
_contributions = contributions;
|
|
67
|
+
_sinteticoSummary = sinteticoSummaryData['summary'];
|
|
68
|
+
_rrspCashBalance = balance;
|
|
69
|
+
_rrspTotalValue = sinteticoSummaryData['total_value'];
|
|
70
|
+
_totalPortfolioBookCost = totalPortfolioBookCost;
|
|
71
|
+
});
|
|
72
|
+
} catch (e) {
|
|
73
|
+
if (!mounted) return;
|
|
74
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
75
|
+
SnackBar(content: Text('Error fetching RRSP data: $e')),
|
|
76
|
+
);
|
|
77
|
+
} finally {
|
|
78
|
+
if (mounted) {
|
|
79
|
+
setState(() {
|
|
80
|
+
_isLoading = false;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Map<String, dynamic> _calculateSinteticoSummary(
|
|
87
|
+
List<Map<String, dynamic>> contributions, double totalPortfolioBookCost) {
|
|
88
|
+
double totalRrsp = 0;
|
|
89
|
+
|
|
90
|
+
double totalDpsp = 0;
|
|
91
|
+
|
|
92
|
+
double totalReturn = 0;
|
|
93
|
+
|
|
94
|
+
for (var c in contributions) {
|
|
95
|
+
totalRrsp += double.parse(c['rrsp_amount'].toString());
|
|
96
|
+
|
|
97
|
+
totalDpsp += double.parse(c['dpsp_amount'].toString());
|
|
98
|
+
|
|
99
|
+
totalReturn += double.parse(c['return_amount']?.toString() ?? '0.0');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
final totalPurchased = totalRrsp + totalDpsp;
|
|
103
|
+
|
|
104
|
+
final bookCost = totalPurchased + totalReturn;
|
|
105
|
+
|
|
106
|
+
final summaryData = {
|
|
107
|
+
'rrsp_amount': totalRrsp,
|
|
108
|
+
'dpsp_amount': totalDpsp,
|
|
109
|
+
'total_purchased': totalPurchased,
|
|
110
|
+
'return_amount': totalReturn,
|
|
111
|
+
'percent_return':
|
|
112
|
+
(totalPurchased > 0) ? (totalReturn / totalPurchased) * 100 : 0.0,
|
|
113
|
+
'book_cost': bookCost,
|
|
114
|
+
'portfolio_allocation': (totalPortfolioBookCost > 0)
|
|
115
|
+
? (bookCost / totalPortfolioBookCost) * 100
|
|
116
|
+
: 0.0,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
'summary': [summaryData],
|
|
121
|
+
'total_value': bookCost
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@override
|
|
126
|
+
Widget build(BuildContext context) {
|
|
127
|
+
return Scaffold(
|
|
128
|
+
appBar: AppBar(
|
|
129
|
+
title: const Text('RRSP Sun Life Portfolio'),
|
|
130
|
+
actions: [
|
|
131
|
+
Center(
|
|
132
|
+
child: Padding(
|
|
133
|
+
padding: const EdgeInsets.only(right: 16.0),
|
|
134
|
+
child: Column(
|
|
135
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
136
|
+
crossAxisAlignment: CrossAxisAlignment.end,
|
|
137
|
+
children: [
|
|
138
|
+
Text(
|
|
139
|
+
'Total Portfolio: ${_formatCurrency(_rrspTotalValue)}',
|
|
140
|
+
style: const TextStyle(
|
|
141
|
+
fontSize: 16, fontWeight: FontWeight.bold),
|
|
142
|
+
),
|
|
143
|
+
Text(
|
|
144
|
+
'Account Balance: ${_formatCurrency(_rrspCashBalance)}',
|
|
145
|
+
style: const TextStyle(fontSize: 12),
|
|
146
|
+
),
|
|
147
|
+
],
|
|
148
|
+
),
|
|
149
|
+
),
|
|
150
|
+
),
|
|
151
|
+
IconButton(
|
|
152
|
+
icon: const Icon(Icons.refresh),
|
|
153
|
+
onPressed: _fetchData,
|
|
154
|
+
tooltip: 'Refresh Data',
|
|
155
|
+
),
|
|
156
|
+
],
|
|
157
|
+
),
|
|
158
|
+
body: _isLoading
|
|
159
|
+
? const Center(child: CircularProgressIndicator())
|
|
160
|
+
: SingleChildScrollView(
|
|
161
|
+
padding: const EdgeInsets.all(16.0),
|
|
162
|
+
child: Column(
|
|
163
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
164
|
+
children: [
|
|
165
|
+
Text('Summary',
|
|
166
|
+
style: Theme.of(context).textTheme.titleLarge),
|
|
167
|
+
const SizedBox(height: 8),
|
|
168
|
+
_buildSinteticoTable(),
|
|
169
|
+
const SizedBox(height: 24),
|
|
170
|
+
RrspContributionForm(
|
|
171
|
+
investmentAccount: 'RRSP Sun Life',
|
|
172
|
+
onContributionLogged: () => _fetchData(),
|
|
173
|
+
token: _token,
|
|
174
|
+
),
|
|
175
|
+
const SizedBox(height: 24),
|
|
176
|
+
Text(
|
|
177
|
+
'Contribution History',
|
|
178
|
+
style: Theme.of(context).textTheme.titleLarge,
|
|
179
|
+
),
|
|
180
|
+
const SizedBox(height: 8),
|
|
181
|
+
_buildContributionTable(),
|
|
182
|
+
],
|
|
183
|
+
),
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
Widget _buildSinteticoTable() {
|
|
189
|
+
if (_sinteticoSummary.isEmpty) {
|
|
190
|
+
return const SizedBox.shrink();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
final summary = _sinteticoSummary.first;
|
|
194
|
+
|
|
195
|
+
return DataTable(
|
|
196
|
+
columns: const [
|
|
197
|
+
DataColumn(label: Text('RRSP')),
|
|
198
|
+
DataColumn(label: Text('DPSP')),
|
|
199
|
+
DataColumn(label: Text('Total Purchased')),
|
|
200
|
+
DataColumn(label: Text('Return')),
|
|
201
|
+
DataColumn(label: Text('% Return')),
|
|
202
|
+
DataColumn(label: Text('Book Cost')),
|
|
203
|
+
],
|
|
204
|
+
rows: [
|
|
205
|
+
DataRow(
|
|
206
|
+
cells: [
|
|
207
|
+
DataCell(Text(_formatCurrency(summary['rrsp_amount'] as double))),
|
|
208
|
+
DataCell(Text(_formatCurrency(summary['dpsp_amount'] as double))),
|
|
209
|
+
DataCell(
|
|
210
|
+
Text(_formatCurrency(summary['total_purchased'] as double))),
|
|
211
|
+
DataCell(Text(_formatCurrency(summary['return_amount'] as double))),
|
|
212
|
+
DataCell(Text(
|
|
213
|
+
'${(summary['percent_return'] as double).toStringAsFixed(2)}%')),
|
|
214
|
+
DataCell(Text(_formatCurrency(summary['book_cost'] as double))),
|
|
215
|
+
],
|
|
216
|
+
),
|
|
217
|
+
],
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
Widget _buildContributionTable() {
|
|
222
|
+
if (_contributions.isEmpty) {
|
|
223
|
+
return const Center(
|
|
224
|
+
child: Padding(
|
|
225
|
+
padding: EdgeInsets.all(16.0),
|
|
226
|
+
child: Text('No contributions found for RRSP Sun Life.'),
|
|
227
|
+
),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return DataTable(
|
|
232
|
+
columns: const [
|
|
233
|
+
DataColumn(label: Text('Date')),
|
|
234
|
+
DataColumn(label: Text('RRSP')),
|
|
235
|
+
DataColumn(label: Text('DPSP')),
|
|
236
|
+
DataColumn(label: Text('Total Purchased')),
|
|
237
|
+
DataColumn(label: Text('Return')),
|
|
238
|
+
DataColumn(label: Text('% Return')),
|
|
239
|
+
DataColumn(label: Text('Book Cost')),
|
|
240
|
+
DataColumn(label: Text('Stock Allocation')),
|
|
241
|
+
DataColumn(label: Text('Actions')), // New column
|
|
242
|
+
],
|
|
243
|
+
rows: _contributions.map((c) {
|
|
244
|
+
final contributionId = c['id'] as int;
|
|
245
|
+
final rrspAmount = double.parse(c['rrsp_amount'].toString());
|
|
246
|
+
final dpspAmount = double.parse(c['dpsp_amount'].toString());
|
|
247
|
+
final totalPurchased = rrspAmount + dpspAmount;
|
|
248
|
+
|
|
249
|
+
final returnAmount =
|
|
250
|
+
double.parse(c['return_amount']?.toString() ?? '0.0');
|
|
251
|
+
final percentReturn =
|
|
252
|
+
totalPurchased > 0 ? (returnAmount / totalPurchased) * 100 : 0.0;
|
|
253
|
+
|
|
254
|
+
final bookCost = totalPurchased + returnAmount;
|
|
255
|
+
|
|
256
|
+
final stockAllocation = _totalPortfolioBookCost > 0
|
|
257
|
+
? (bookCost / _totalPortfolioBookCost) * 100
|
|
258
|
+
: 0.0;
|
|
259
|
+
|
|
260
|
+
String dateStr;
|
|
261
|
+
final rawDate = c['date'];
|
|
262
|
+
if (rawDate is String) {
|
|
263
|
+
dateStr = DateFormat('yyyy-MM-dd').format(DateTime.parse(rawDate));
|
|
264
|
+
} else if (rawDate is DateTime) {
|
|
265
|
+
dateStr = DateFormat('yyyy-MM-dd').format(rawDate);
|
|
266
|
+
} else {
|
|
267
|
+
dateStr = '-';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return DataRow(
|
|
271
|
+
cells: [
|
|
272
|
+
DataCell(Text(dateStr)),
|
|
273
|
+
DataCell(Text(_formatCurrency(rrspAmount))),
|
|
274
|
+
DataCell(Text(_formatCurrency(dpspAmount))),
|
|
275
|
+
DataCell(Text(_formatCurrency(totalPurchased))),
|
|
276
|
+
DataCell(Text(_formatCurrency(returnAmount))),
|
|
277
|
+
DataCell(Text('${percentReturn.toStringAsFixed(2)}%')),
|
|
278
|
+
DataCell(Text(_formatCurrency(bookCost))),
|
|
279
|
+
DataCell(Text('${stockAllocation.toStringAsFixed(2)}%')),
|
|
280
|
+
DataCell( // New cell for the delete button
|
|
281
|
+
IconButton(
|
|
282
|
+
icon: const Icon(Icons.delete, color: Colors.red),
|
|
283
|
+
onPressed: () => _confirmAndDeleteContribution(contributionId),
|
|
284
|
+
tooltip: 'Delete Contribution',
|
|
285
|
+
),
|
|
286
|
+
),
|
|
287
|
+
],
|
|
288
|
+
);
|
|
289
|
+
}).toList(),
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
Future<void> _confirmAndDeleteContribution(int contributionId) async {
|
|
294
|
+
final bool? confirm = await showDialog<bool>(
|
|
295
|
+
context: context,
|
|
296
|
+
builder: (BuildContext context) {
|
|
297
|
+
return AlertDialog(
|
|
298
|
+
title: const Text('Confirm Deletion'),
|
|
299
|
+
content: const Text('Are you sure you want to delete this contribution? This action cannot be undone.'),
|
|
300
|
+
actions: <Widget>[
|
|
301
|
+
TextButton(
|
|
302
|
+
onPressed: () => Navigator.of(context).pop(false),
|
|
303
|
+
child: const Text('Cancel'),
|
|
304
|
+
),
|
|
305
|
+
TextButton(
|
|
306
|
+
onPressed: () => Navigator.of(context).pop(true),
|
|
307
|
+
child: const Text('Delete'),
|
|
308
|
+
),
|
|
309
|
+
],
|
|
310
|
+
);
|
|
311
|
+
},
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
if (confirm == true) {
|
|
315
|
+
if (_token == null) {
|
|
316
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
317
|
+
const SnackBar(content: Text('Authentication token not available.')),
|
|
318
|
+
);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
final success = await _transactionService.deleteRrspContribution(contributionId, token: _token);
|
|
323
|
+
if (success) {
|
|
324
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
325
|
+
const SnackBar(content: Text('Contribution deleted successfully.')),
|
|
326
|
+
);
|
|
327
|
+
_fetchData(); // Refresh data after deletion
|
|
328
|
+
} else {
|
|
329
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
330
|
+
const SnackBar(content: Text('Failed to delete contribution.')),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
} catch (e) {
|
|
334
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
335
|
+
SnackBar(content: Text('Error deleting contribution: $e')),
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@marcos_feitoza/personal-finance-frontend-feature-investments",
|
|
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,59 @@
|
|
|
1
|
+
name: personal_finance_frontend_feature_investments
|
|
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
|
+
|
|
19
|
+
dev_dependencies:
|
|
20
|
+
flutter_test:
|
|
21
|
+
sdk: flutter
|
|
22
|
+
flutter_lints: ^5.0.0
|
|
23
|
+
|
|
24
|
+
# For information on the generic Dart part of this file, see the
|
|
25
|
+
# following page: https://dart.dev/tools/pub/pubspec
|
|
26
|
+
|
|
27
|
+
# The following section is specific to Flutter packages.
|
|
28
|
+
flutter:
|
|
29
|
+
|
|
30
|
+
# To add assets to your package, add an assets section, like this:
|
|
31
|
+
# assets:
|
|
32
|
+
# - images/a_dot_burr.jpeg
|
|
33
|
+
# - images/a_dot_ham.jpeg
|
|
34
|
+
#
|
|
35
|
+
# For details regarding assets in packages, see
|
|
36
|
+
# https://flutter.dev/to/asset-from-package
|
|
37
|
+
#
|
|
38
|
+
# An image asset can refer to one or more resolution-specific "variants", see
|
|
39
|
+
# https://flutter.dev/to/resolution-aware-images
|
|
40
|
+
|
|
41
|
+
# To add custom fonts to your package, add a fonts section here,
|
|
42
|
+
# in this "flutter" section. Each entry in this list should have a
|
|
43
|
+
# "family" key with the font family name, and a "fonts" key with a
|
|
44
|
+
# list giving the asset and other descriptors for the font. For
|
|
45
|
+
# example:
|
|
46
|
+
# fonts:
|
|
47
|
+
# - family: Schyler
|
|
48
|
+
# fonts:
|
|
49
|
+
# - asset: fonts/Schyler-Regular.ttf
|
|
50
|
+
# - asset: fonts/Schyler-Italic.ttf
|
|
51
|
+
# style: italic
|
|
52
|
+
# - family: Trajan Pro
|
|
53
|
+
# fonts:
|
|
54
|
+
# - asset: fonts/TrajanPro.ttf
|
|
55
|
+
# - asset: fonts/TrajanPro_Bold.ttf
|
|
56
|
+
# weight: 700
|
|
57
|
+
#
|
|
58
|
+
# For details regarding fonts in packages, see
|
|
59
|
+
# 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_investments/personal_finance_frontend_feature_investments.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
|
+
}
|