@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.
@@ -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,30 @@
1
+ # 1.0.0 (2025-11-22)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * add back dividend to invest table ([2000d5f](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/2000d5fdf9c9ab86d2a60a4409c4f610b9ca7bfa))
7
+ * add crypto live price working ([20c34ff](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/20c34ff420058b58e9ac638acf831497d15f9a3e))
8
+ * add currency class ([0537b72](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/0537b727baec9591d69a52c82a56b02688300dd4))
9
+ * add date logs ([5fd928d](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/5fd928d3e2b5ad1998ebadaf5fce3dee614a743a))
10
+ * add ignore and more estilo ([3d21098](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/3d2109816a6e53baf076767bc3b36d056ae0bd11))
11
+ * add more logs ([6b1274a](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/6b1274abd27f649186a1e89a328120eff91b70e1))
12
+ * add release files ([630829b](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/630829b7aa6ad7af426f766081aa9c8eb3f547c8))
13
+ * add rrsp sl table back ([3bf38c6](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/3bf38c6dd4abd8fd9267e138b5c2a434bcc9d65f))
14
+ * add sell option back to crypto ([38e7118](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/38e7118b0c008cfd2a534d6f4ced9c05c814b0ee))
15
+ * auth_provider ([312c293](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/312c2935b509550844be6791e84647d21dad9ac0))
16
+ * decimal format ([f67454f](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/f67454f94018c2c4461d6a7d6fc0d602a33731a4))
17
+ * fix stock market ([9cfd689](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/9cfd689602878bfd25e00ac2f4cc3d70c0a5a4da))
18
+ * remove name table of rrsp tfsa and no-reg ([6cf11a4](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/6cf11a404dd9dd6b80cce7fa48ffb6adbfaabee8))
19
+ * rrsp sl new estilo ([5de7f4a](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/5de7f4a056e6cbc83f1feb8e785bacd46ee04f8e))
20
+ * validation of crypt working ([737502a](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/737502a053a2a1c4039b3b380cb49ee7f04be334))
21
+
22
+
23
+ ### Features
24
+
25
+ * add delete crypto position ([1056be8](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/1056be8f2c08941aefa8b83a9bb60f278eb5e026))
26
+ * Schema for updating a Trade ([2bca5e8](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/2bca5e80fc57198bc377ba3b8cff59eae41e5d2d))
27
+
28
+ ## 0.0.1
29
+
30
+ * 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,4 @@
1
+ include: package:flutter_lints/flutter.yaml
2
+
3
+ # Additional information about this file can be found at
4
+ # https://dart.dev/guides/language/analysis-options
@@ -0,0 +1,5 @@
1
+ /// A Calculator.
2
+ class Calculator {
3
+ /// Returns [value] plus 1.
4
+ int addOne(int value) => value + 1;
5
+ }
@@ -0,0 +1,381 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:intl/intl.dart';
3
+ import 'package:personal_finance_frontend_core_services/services/crypto_service.dart';
4
+ import 'package:personal_finance_frontend_core_services/services/transaction_service.dart';
5
+ import 'package:personal_finance_frontend_core_ui/widgets/crypto_trade_form.dart';
6
+ import 'package:personal_finance_frontend_core_ui/widgets/app_widgets.dart';
7
+ import 'package:provider/provider.dart';
8
+ import 'package:personal_finance_frontend_core_services/providers/auth_provider.dart';
9
+
10
+ class CryptoAccountScreen extends StatefulWidget {
11
+ final String accountName;
12
+
13
+ const CryptoAccountScreen({Key? key, required this.accountName}) : super(key: key);
14
+
15
+ @override
16
+ _CryptoAccountScreenState createState() => _CryptoAccountScreenState();
17
+ }
18
+
19
+ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
20
+ final TransactionService _transactionService = TransactionService();
21
+ final CryptoService _cryptoService = CryptoService();
22
+
23
+ List<Map<String, dynamic>> _trades = [];
24
+ List<Map<String, dynamic>> _assets = [];
25
+ List<Map<String, dynamic>> _portfolioSummary = [];
26
+ Map<String, double> _livePrices = {};
27
+ double _cashBalance = 0.0;
28
+ double _accountTotalValue = 0.0;
29
+ double _totalPortfolioBookCost = 0.0;
30
+ bool _isLoading = true;
31
+ bool _isFetchingPrices = false;
32
+ String? _selectedSymbolForFilter;
33
+ String? _token;
34
+
35
+ @override
36
+ void initState() {
37
+ super.initState();
38
+ WidgetsBinding.instance.addPostFrameCallback((_) {
39
+ final authProvider = Provider.of<AuthProvider>(context, listen: false);
40
+ setState(() {
41
+ _token = authProvider.token;
42
+ });
43
+ _fetchData();
44
+ });
45
+ }
46
+
47
+ String currencySymbol = r'$';
48
+
49
+ String _formatCurrency(double value) {
50
+ return '$currencySymbol${value.toStringAsFixed(2)}';
51
+ }
52
+
53
+ Future<void> _fetchData() async {
54
+ if (_token == null) return;
55
+ setState(() => _isLoading = true);
56
+ try {
57
+ final futures = <Future>[
58
+ _transactionService.getTrades(investmentAccount: widget.accountName, token: _token),
59
+ _transactionService.getAccountBalance(widget.accountName, token: _token),
60
+ _transactionService.getAssets(investmentAccount: widget.accountName, token: _token),
61
+ _transactionService.getTotalPortfolioBookCost(token: _token), // Fetch total cost
62
+ ];
63
+ final results = await Future.wait(futures);
64
+
65
+ final trades = results[0] as List<Map<String, dynamic>>;
66
+ final balance = results[1] as double;
67
+ final assets = results[2] as List<Map<String, dynamic>>;
68
+ final totalPortfolioBookCost = results[3] as double;
69
+
70
+ final summaryData = _calculatePortfolioSummary(trades, totalPortfolioBookCost);
71
+
72
+ setState(() {
73
+ _trades = trades;
74
+ _cashBalance = balance;
75
+ _assets = assets;
76
+ _portfolioSummary = summaryData['summary'];
77
+ _accountTotalValue = summaryData['total_value'] + _cashBalance;
78
+ _totalPortfolioBookCost = totalPortfolioBookCost;
79
+ });
80
+
81
+ if (mounted) await _fetchLivePrices();
82
+ } catch (e) {
83
+ if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error fetching data: $e')));
84
+ } finally {
85
+ if (mounted) setState(() => _isLoading = false);
86
+ }
87
+ }
88
+
89
+ Future<void> _fetchLivePrices() async {
90
+ if (_portfolioSummary.isEmpty || !mounted) return;
91
+ setState(() => _isFetchingPrices = true);
92
+
93
+ final idsToFetch = _portfolioSummary.map((p) => p['id_crypto'] as String? ?? p['symbol'] as String).where((s) => s.isNotEmpty).toList();
94
+
95
+ if (idsToFetch.isEmpty) {
96
+ setState(() => _isFetchingPrices = false);
97
+ return;
98
+ }
99
+
100
+ final livePrices = await _cryptoService.getLiveCryptoPrices(idsToFetch);
101
+
102
+ if (!mounted) return;
103
+
104
+ double newTotalValue = _cashBalance;
105
+ for (final position in _portfolioSummary) {
106
+ final shares = double.parse(position['shares'].toString());
107
+ final idForLookup = (position['id_crypto'] as String? ?? position['symbol'] as String).toLowerCase();
108
+ final livePrice = livePrices[idForLookup];
109
+ if (livePrice != null) {
110
+ newTotalValue += shares * livePrice;
111
+ } else {
112
+ newTotalValue += (position['total_cost'] as num?)?.toDouble() ?? 0.0;
113
+ }
114
+ }
115
+
116
+ setState(() {
117
+ _livePrices = livePrices;
118
+ _accountTotalValue = newTotalValue;
119
+ _isFetchingPrices = false;
120
+ });
121
+ }
122
+
123
+ Map<String, dynamic> _calculatePortfolioSummary(List<Map<String, dynamic>> trades, double totalPortfolioBookCost) {
124
+ Map<String, dynamic> summary = {};
125
+ double accountPortfolioBookCost = 0;
126
+
127
+ for (var trade in trades) {
128
+ final asset = trade['asset'];
129
+ if (asset == null || asset['symbol'] == null) continue;
130
+ String symbol = asset['symbol'];
131
+ String idCrypto = asset['id_crypto'] ?? symbol;
132
+
133
+ double shares = double.parse(trade['shares'].toString());
134
+ double price = double.parse(trade['price'].toString());
135
+ String tradeType = trade['trade_type'];
136
+
137
+ if (!summary.containsKey(symbol)) {
138
+ summary[symbol] = {
139
+ 'symbol': symbol,
140
+ 'id_crypto': idCrypto,
141
+ 'name': asset['name'] ?? symbol,
142
+ 'shares': 0.0,
143
+ 'total_cost': 0.0,
144
+ };
145
+ }
146
+
147
+ if (tradeType == 'buy') {
148
+ summary[symbol]['shares'] += shares;
149
+ summary[symbol]['total_cost'] += shares * price;
150
+ } else if (tradeType == 'sell') {
151
+ double originalShares = summary[symbol]['shares'];
152
+ if (originalShares > 0) {
153
+ double avgPrice = summary[symbol]['total_cost'] / originalShares;
154
+ summary[symbol]['total_cost'] -= shares * avgPrice;
155
+ }
156
+ summary[symbol]['shares'] -= shares;
157
+ }
158
+ }
159
+
160
+ summary.removeWhere((key, value) => value['shares'] < 0.01);
161
+ accountPortfolioBookCost = summary.values.fold(0.0, (sum, item) => sum + item['total_cost']);
162
+
163
+ List<Map<String, dynamic>> result = [];
164
+ summary.forEach((symbol, data) {
165
+ double shares = data['shares'];
166
+ double totalCost = data['total_cost'];
167
+ data['avg_price'] = (shares > 0) ? totalCost / shares : 0.0;
168
+ data['account_allocation'] = (accountPortfolioBookCost > 0) ? (totalCost / accountPortfolioBookCost) * 100 : 0.0;
169
+ data['stocks_allocation'] = (totalPortfolioBookCost > 0) ? (totalCost / totalPortfolioBookCost) * 100 : 0.0;
170
+ result.add(data);
171
+ });
172
+
173
+ return {'summary': result, 'total_value': accountPortfolioBookCost};
174
+ }
175
+
176
+ @override
177
+ Widget build(BuildContext context) {
178
+ return Scaffold(
179
+ appBar: AppBar(
180
+ title: Text('${widget.accountName} Portfolio'),
181
+ actions: [
182
+ Center(
183
+ child: Padding(
184
+ padding: const EdgeInsets.only(right: 16.0),
185
+ child: Column(
186
+ mainAxisAlignment: MainAxisAlignment.center,
187
+ crossAxisAlignment: CrossAxisAlignment.end,
188
+ children: [
189
+ Text('Total Portfolio: ${_formatCurrency(_accountTotalValue)}', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
190
+ Text('Account Balance: ${_formatCurrency(_cashBalance)}', style: const TextStyle(fontSize: 12)),
191
+ ],
192
+ ),
193
+ ),
194
+ ),
195
+ IconButton(icon: const Icon(Icons.refresh), onPressed: _fetchData, tooltip: 'Refresh Data'),
196
+ ],
197
+ ),
198
+ body: _isLoading
199
+ ? const Center(child: CircularProgressIndicator())
200
+ : RefreshIndicator(
201
+ onRefresh: _fetchData,
202
+ child: SingleChildScrollView(
203
+ physics: const AlwaysScrollableScrollPhysics(),
204
+ padding: const EdgeInsets.all(16.0),
205
+ child: Column(
206
+ crossAxisAlignment: CrossAxisAlignment.stretch,
207
+ children: [
208
+ Row(
209
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
210
+ children: [
211
+ Text('Portfolio Summary', style: Theme.of(context).textTheme.titleLarge),
212
+ if (_isFetchingPrices) const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2.0)),
213
+ ],
214
+ ),
215
+ _buildSinteticoTable(),
216
+ const SizedBox(height: 24),
217
+ CryptoTradeForm(
218
+ accountName: widget.accountName,
219
+ portfolioSummary: _portfolioSummary,
220
+ onTradeCreated: (_) => _fetchData(),
221
+ token: _token),
222
+ const SizedBox(height: 24),
223
+ Text('Trade History', style: Theme.of(context).textTheme.titleLarge),
224
+ const SizedBox(height: 8),
225
+ _buildAnaliticoTable(),
226
+ ],
227
+ ),
228
+ ),
229
+ ),
230
+ );
231
+ }
232
+
233
+ Widget _buildSinteticoTable() {
234
+ if (_portfolioSummary.isEmpty) return const Center(child: Padding(padding: EdgeInsets.all(16.0), child: Text('No positions held.')));
235
+
236
+ return SingleChildScrollView(
237
+ scrollDirection: Axis.horizontal,
238
+ child: DataTable(
239
+ columnSpacing: 24.0,
240
+ columns: const [
241
+ DataColumn(label: Text('Name')),
242
+ DataColumn(label: Text('Symbol')),
243
+ DataColumn(label: Text('Quantity')),
244
+ DataColumn(label: Text('Avg Price')),
245
+ DataColumn(label: Text('Live')),
246
+ DataColumn(label: Text('Book Cost')),
247
+ DataColumn(label: Text('Allocation')),
248
+ DataColumn(label: Text('Stocks Allocation')),
249
+ DataColumn(label: Text('Return')),
250
+ DataColumn(label: Text('% Return')),
251
+ ],
252
+ rows: _portfolioSummary.map((position) {
253
+ final symbol = position['symbol'] as String;
254
+ final idForLookup = (position['id_crypto'] as String? ?? symbol).toLowerCase();
255
+ final livePrice = _livePrices[idForLookup];
256
+ final shares = double.parse(position['shares'].toString());
257
+ final avgPrice = double.parse(position['avg_price'].toString());
258
+ final totalPurchased = double.parse(position['total_cost'].toString());
259
+ final accountAllocation = (position['account_allocation'] as num?)?.toDouble() ?? 0.0;
260
+ final stocksAllocation = (position['stocks_allocation'] as num?)?.toDouble() ?? 0.0;
261
+
262
+ double? totalReturnValue, percentageReturn;
263
+
264
+ if (livePrice != null && shares > 0) {
265
+ final currentMarketValue = livePrice * shares;
266
+ totalReturnValue = currentMarketValue - totalPurchased;
267
+ if (totalPurchased > 0) percentageReturn = (totalReturnValue / totalPurchased) * 100;
268
+ }
269
+
270
+ final returnColor = (totalReturnValue ?? 0) >= 0 ? Colors.green : Colors.red;
271
+
272
+ return DataRow(cells: [
273
+ DataCell(SizedBox(width: 150, child: Text(position['name'] as String, overflow: TextOverflow.ellipsis))),
274
+ DataCell(Text(symbol)),
275
+ DataCell(Text(shares.toStringAsFixed(6))),
276
+ DataCell(Text(_formatCurrency(avgPrice))),
277
+ DataCell(livePrice != null ? Text(_formatCurrency(livePrice)) : const Text('N/A')),
278
+ DataCell(Text(_formatCurrency(totalPurchased))),
279
+ DataCell(Text('${accountAllocation.toStringAsFixed(2)}%')),
280
+ DataCell(Text('${stocksAllocation.toStringAsFixed(2)}%')),
281
+ DataCell(totalReturnValue != null ? Text(_formatCurrency(totalReturnValue), style: TextStyle(color: returnColor)) : const Text('N/A')),
282
+ DataCell(percentageReturn != null ? Text('${percentageReturn.toStringAsFixed(2)}%', style: TextStyle(color: returnColor)) : const Text('N/A')),
283
+ ]);
284
+ }).toList(),
285
+ ),
286
+ );
287
+ }
288
+
289
+ Widget _buildAnaliticoTable() {
290
+ if (_trades.isEmpty) return const Center(child: Padding(padding: EdgeInsets.all(16.0), child: Text('No trades found.')));
291
+
292
+ final sortedTrades = List<Map<String, dynamic>>.from(_trades)..sort((a, b) {
293
+ try {
294
+ return DateTime.parse(b['date'] as String).compareTo(DateTime.parse(a['date'] as String));
295
+ } catch (e) { return 0; }
296
+ });
297
+
298
+ return DataTable(
299
+ columns: const [
300
+ DataColumn(label: Text('Date')),
301
+ DataColumn(label: Text('Symbol')),
302
+ DataColumn(label: Text('Type')),
303
+ DataColumn(label: Text('Quantity')),
304
+ DataColumn(label: Text('Price')),
305
+ DataColumn(label: Text('Total')),
306
+ DataColumn(label: Text('Actions')), // New column
307
+ ],
308
+ rows: sortedTrades.map((trade) {
309
+ final double shares = double.parse(trade['shares'].toString());
310
+ final double price = double.parse(trade['price'].toString());
311
+ final double total = shares * price;
312
+ final tradeDate = DateTime.parse(trade['date'] as String);
313
+ final int tradeId = trade['id'] as int; // Get trade ID
314
+
315
+ return DataRow(cells: [
316
+ DataCell(Text(DateFormat('yyyy-MM-dd').format(tradeDate))),
317
+ DataCell(Text(trade['asset']?['symbol'] ?? 'N/A')),
318
+ DataCell(Text(trade['trade_type'] ?? 'N/A')),
319
+ DataCell(Text(shares.toStringAsFixed(6))),
320
+ DataCell(Text(_formatCurrency(price))),
321
+ DataCell(Text(_formatCurrency(total))),
322
+ DataCell( // New cell for the delete button
323
+ IconButton(
324
+ icon: const Icon(Icons.delete, color: Colors.red),
325
+ onPressed: () => _confirmAndDeleteTrade(tradeId),
326
+ tooltip: 'Delete Trade',
327
+ ),
328
+ ),
329
+ ]);
330
+ }).toList(),
331
+ );
332
+ }
333
+
334
+ Future<void> _confirmAndDeleteTrade(int tradeId) async {
335
+ final bool? confirm = await showDialog<bool>(
336
+ context: context,
337
+ builder: (BuildContext context) {
338
+ return AlertDialog(
339
+ title: const Text('Confirm Deletion'),
340
+ content: const Text('Are you sure you want to delete this trade? This action cannot be undone.'),
341
+ actions: <Widget>[
342
+ TextButton(
343
+ onPressed: () => Navigator.of(context).pop(false),
344
+ child: const Text('Cancel'),
345
+ ),
346
+ TextButton(
347
+ onPressed: () => Navigator.of(context).pop(true),
348
+ child: const Text('Delete'),
349
+ ),
350
+ ],
351
+ );
352
+ },
353
+ );
354
+
355
+ if (confirm == true) {
356
+ if (_token == null) {
357
+ ScaffoldMessenger.of(context).showSnackBar(
358
+ const SnackBar(content: Text('Authentication token not available.')),
359
+ );
360
+ return;
361
+ }
362
+ try {
363
+ final success = await _transactionService.deleteTrade(tradeId, token: _token);
364
+ if (success) {
365
+ ScaffoldMessenger.of(context).showSnackBar(
366
+ const SnackBar(content: Text('Trade deleted successfully.')),
367
+ );
368
+ _fetchData(); // Refresh data after deletion
369
+ } else {
370
+ ScaffoldMessenger.of(context).showSnackBar(
371
+ const SnackBar(content: Text('Failed to delete trade.')),
372
+ );
373
+ }
374
+ } catch (e) {
375
+ ScaffoldMessenger.of(context).showSnackBar(
376
+ SnackBar(content: Text('Error deleting trade: $e')),
377
+ );
378
+ }
379
+ }
380
+ }
381
+ }