@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,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,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
|
+
}
|