@marcos_feitoza/personal-finance-frontend-feature-investments 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/lib/screens/crypto_account_screen.dart +171 -281
- package/lib/screens/investment_account_screen.dart +217 -402
- package/lib/screens/rrsp_sun_life_screen.dart +119 -224
- package/lib/viewmodels/crypto_account_viewmodel.dart +180 -0
- package/lib/viewmodels/investment_account_viewmodel.dart +221 -0
- package/lib/viewmodels/rrsp_sun_life_viewmodel.dart +103 -0
- package/package.json +1 -1
|
@@ -1,59 +1,137 @@
|
|
|
1
1
|
import 'package:flutter/material.dart';
|
|
2
2
|
import 'package:intl/intl.dart';
|
|
3
|
-
import 'package:personal_finance_frontend_core_services/services/stock_service.dart';
|
|
4
|
-
import 'package:personal_finance_frontend_core_services/services/transaction_service.dart';
|
|
5
|
-
import 'package:personal_finance_frontend_core_ui/widgets/trade_form.dart';
|
|
6
|
-
import 'package:personal_finance_frontend_core_ui/widgets/dividend_log_form.dart';
|
|
7
|
-
import 'package:personal_finance_frontend_core_ui/widgets/app_widgets.dart';
|
|
8
3
|
import 'package:provider/provider.dart';
|
|
9
4
|
import 'package:personal_finance_frontend_core_services/providers/auth_provider.dart';
|
|
10
5
|
import 'package:personal_finance_frontend_core_ui/utils/app_dialogs.dart';
|
|
6
|
+
import 'package:personal_finance_frontend_core_ui/widgets/trade_form.dart';
|
|
7
|
+
import 'package:personal_finance_frontend_core_ui/widgets/dividend_log_form.dart';
|
|
8
|
+
import 'package:personal_finance_frontend_core_ui/widgets/app_widgets.dart';
|
|
9
|
+
import '../viewmodels/investment_account_viewmodel.dart';
|
|
11
10
|
|
|
12
|
-
class InvestmentAccountScreen extends
|
|
11
|
+
class InvestmentAccountScreen extends StatelessWidget {
|
|
13
12
|
final String accountName;
|
|
14
13
|
final bool showDividends;
|
|
15
14
|
|
|
16
|
-
const InvestmentAccountScreen(
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
const InvestmentAccountScreen({
|
|
16
|
+
Key? key,
|
|
17
|
+
required this.accountName,
|
|
18
|
+
this.showDividends = true,
|
|
19
|
+
}) : super(key: key);
|
|
19
20
|
|
|
20
21
|
@override
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
26
|
-
final TransactionService _transactionService = TransactionService();
|
|
27
|
-
final StockService _stockService = StockService();
|
|
28
|
-
|
|
29
|
-
List<Map<String, dynamic>> _trades = [];
|
|
30
|
-
List<Map<String, dynamic>> _dividends = [];
|
|
31
|
-
List<Map<String, dynamic>> _assets = [];
|
|
32
|
-
List<Map<String, dynamic>> _portfolioSummary = [];
|
|
33
|
-
Map<String, double> _livePrices = {};
|
|
34
|
-
double _cashBalance = 0.0;
|
|
35
|
-
double _accountTotalValue = 0.0;
|
|
36
|
-
double _totalPortfolioBookCost = 0.0;
|
|
37
|
-
bool _isLoading = true;
|
|
38
|
-
bool _isFetchingPrices = false;
|
|
39
|
-
String? _selectedSymbolForFilter;
|
|
40
|
-
String? _token;
|
|
22
|
+
Widget build(BuildContext context) {
|
|
23
|
+
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
|
41
24
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
25
|
+
return ChangeNotifierProvider(
|
|
26
|
+
create: (_) => InvestmentAccountViewModel(
|
|
27
|
+
accountName: accountName,
|
|
28
|
+
showDividends: showDividends,
|
|
29
|
+
token: authProvider.token,
|
|
30
|
+
),
|
|
31
|
+
child: Consumer<InvestmentAccountViewModel>(
|
|
32
|
+
builder: (context, viewModel, child) {
|
|
33
|
+
return Scaffold(
|
|
34
|
+
appBar: AppBar(
|
|
35
|
+
title: Text('$accountName Portfolio'),
|
|
36
|
+
actions: [
|
|
37
|
+
Center(
|
|
38
|
+
child: Padding(
|
|
39
|
+
padding: const EdgeInsets.only(right: 16.0),
|
|
40
|
+
child: Column(
|
|
41
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
42
|
+
crossAxisAlignment: CrossAxisAlignment.end,
|
|
43
|
+
children: [
|
|
44
|
+
Text(
|
|
45
|
+
'Total Portfolio: ${_formatCurrency(viewModel.accountTotalValue)}',
|
|
46
|
+
style: const TextStyle(
|
|
47
|
+
fontSize: 16, fontWeight: FontWeight.bold),
|
|
48
|
+
),
|
|
49
|
+
Text(
|
|
50
|
+
'Account Balance: ${_formatCurrency(viewModel.cashBalance)}',
|
|
51
|
+
style: const TextStyle(fontSize: 12),
|
|
52
|
+
),
|
|
53
|
+
],
|
|
54
|
+
),
|
|
55
|
+
),
|
|
56
|
+
),
|
|
57
|
+
IconButton(
|
|
58
|
+
icon: const Icon(Icons.refresh),
|
|
59
|
+
onPressed: viewModel.fetchData,
|
|
60
|
+
tooltip: 'Refresh Data',
|
|
61
|
+
),
|
|
62
|
+
],
|
|
63
|
+
),
|
|
64
|
+
body: viewModel.isLoading
|
|
65
|
+
? const Center(child: CircularProgressIndicator())
|
|
66
|
+
: RefreshIndicator(
|
|
67
|
+
onRefresh: viewModel.fetchData,
|
|
68
|
+
child: SingleChildScrollView(
|
|
69
|
+
physics: const AlwaysScrollableScrollPhysics(),
|
|
70
|
+
padding: const EdgeInsets.all(16.0),
|
|
71
|
+
child: Column(
|
|
72
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
73
|
+
children: [
|
|
74
|
+
Row(
|
|
75
|
+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
76
|
+
children: [
|
|
77
|
+
Text('Portfolio Summary (Sintético)',
|
|
78
|
+
style:
|
|
79
|
+
Theme.of(context).textTheme.titleLarge),
|
|
80
|
+
if (viewModel.isFetchingPrices)
|
|
81
|
+
const SizedBox(
|
|
82
|
+
height: 20,
|
|
83
|
+
width: 20,
|
|
84
|
+
child: CircularProgressIndicator(
|
|
85
|
+
strokeWidth: 2.0),
|
|
86
|
+
),
|
|
87
|
+
],
|
|
88
|
+
),
|
|
89
|
+
_buildSinteticoTable(context, viewModel),
|
|
90
|
+
const SizedBox(height: 24),
|
|
91
|
+
Row(
|
|
92
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
93
|
+
children: [
|
|
94
|
+
Expanded(
|
|
95
|
+
child: TradeForm(
|
|
96
|
+
accountName: accountName,
|
|
97
|
+
portfolioSummary: viewModel.portfolioSummary,
|
|
98
|
+
assets: viewModel.assets,
|
|
99
|
+
onTradeCreated: (_) => viewModel.fetchData(),
|
|
100
|
+
token: viewModel.token,
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
if (showDividends) ...[
|
|
104
|
+
const SizedBox(width: 16),
|
|
105
|
+
Expanded(
|
|
106
|
+
child: DividendLogForm(
|
|
107
|
+
investmentAccount: accountName,
|
|
108
|
+
assets: viewModel.assets,
|
|
109
|
+
onDividendLogged: viewModel.fetchData,
|
|
110
|
+
token: viewModel.token,
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
],
|
|
114
|
+
],
|
|
115
|
+
),
|
|
116
|
+
const SizedBox(height: 24),
|
|
117
|
+
Text('Trade History (Analítico)',
|
|
118
|
+
style: Theme.of(context).textTheme.titleLarge),
|
|
119
|
+
const SizedBox(height: 8),
|
|
120
|
+
_buildAnaliticoFilter(context, viewModel),
|
|
121
|
+
const SizedBox(height: 8),
|
|
122
|
+
_buildAnaliticoTable(context, viewModel),
|
|
123
|
+
],
|
|
124
|
+
),
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
),
|
|
130
|
+
);
|
|
52
131
|
}
|
|
53
132
|
|
|
54
|
-
String currencySymbol = r'$';
|
|
55
|
-
|
|
56
133
|
String _formatCurrency(double value) {
|
|
134
|
+
String currencySymbol = r'$';
|
|
57
135
|
return '$currencySymbol${value.toStringAsFixed(2)}';
|
|
58
136
|
}
|
|
59
137
|
|
|
@@ -71,287 +149,16 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
71
149
|
return parts.join(' ');
|
|
72
150
|
}
|
|
73
151
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
final futures = <Future>[
|
|
79
|
-
_transactionService.getTrades(investmentAccount: widget.accountName, token: _token),
|
|
80
|
-
_transactionService.getAccountBalance(widget.accountName, token: _token),
|
|
81
|
-
_transactionService.getAssets(investmentAccount: widget.accountName, token: _token),
|
|
82
|
-
_transactionService.getTotalPortfolioBookCost(token: _token),
|
|
83
|
-
];
|
|
84
|
-
if (widget.showDividends) {
|
|
85
|
-
futures.add(_transactionService.getDividends(
|
|
86
|
-
investmentAccount: widget.accountName, token: _token));
|
|
87
|
-
}
|
|
88
|
-
final results = await Future.wait(futures);
|
|
89
|
-
|
|
90
|
-
final trades = results[0] as List<Map<String, dynamic>>;
|
|
91
|
-
final balance = results[1] as double;
|
|
92
|
-
final assets = results[2] as List<Map<String, dynamic>>;
|
|
93
|
-
final totalPortfolioBookCost = results[3] as double;
|
|
94
|
-
final dividends = widget.showDividends
|
|
95
|
-
? results[4] as List<Map<String, dynamic>>
|
|
96
|
-
: <Map<String, dynamic>>[];
|
|
97
|
-
|
|
98
|
-
final summaryData = _calculatePortfolioSummary(trades, dividends);
|
|
99
|
-
|
|
100
|
-
setState(() {
|
|
101
|
-
_trades = trades;
|
|
102
|
-
_dividends = dividends;
|
|
103
|
-
_cashBalance = balance;
|
|
104
|
-
_assets = assets;
|
|
105
|
-
_portfolioSummary = summaryData['summary'];
|
|
106
|
-
_accountTotalValue = summaryData['total_value'] + _cashBalance;
|
|
107
|
-
_totalPortfolioBookCost = totalPortfolioBookCost;
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
if (mounted) await _fetchLivePrices();
|
|
111
|
-
} catch (e) {
|
|
112
|
-
if (mounted)
|
|
113
|
-
ScaffoldMessenger.of(context)
|
|
114
|
-
.showSnackBar(SnackBar(content: Text('Error fetching data: $e')));
|
|
115
|
-
} finally {
|
|
116
|
-
if (mounted) setState(() => _isLoading = false);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
Future<void> _fetchLivePrices() async {
|
|
121
|
-
if (_portfolioSummary.isEmpty || !mounted) return;
|
|
122
|
-
setState(() => _isFetchingPrices = true);
|
|
123
|
-
|
|
124
|
-
final symbolsToFetch = _portfolioSummary
|
|
152
|
+
Widget _buildAnaliticoFilter(
|
|
153
|
+
BuildContext context, InvestmentAccountViewModel viewModel) {
|
|
154
|
+
final symbols = viewModel.portfolioSummary
|
|
125
155
|
.map((p) => p['symbol'] as String)
|
|
126
|
-
.
|
|
156
|
+
.toSet()
|
|
127
157
|
.toList();
|
|
128
|
-
|
|
129
|
-
if (symbolsToFetch.isEmpty) {
|
|
130
|
-
setState(() => _isFetchingPrices = false);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
final livePrices = await _stockService.getLivePrices(symbolsToFetch);
|
|
135
|
-
|
|
136
|
-
if (!mounted) return;
|
|
137
|
-
|
|
138
|
-
double newTotalValue = _cashBalance;
|
|
139
|
-
for (final position in _portfolioSummary) {
|
|
140
|
-
final shares = double.parse(position['shares'].toString());
|
|
141
|
-
final livePrice = livePrices[position['symbol']];
|
|
142
|
-
if (livePrice != null) {
|
|
143
|
-
newTotalValue += shares * livePrice;
|
|
144
|
-
} else {
|
|
145
|
-
// If live price is not available, use book cost as a fallback for market value
|
|
146
|
-
newTotalValue += (position['book_cost'] as num?)?.toDouble() ?? 0.0;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
setState(() {
|
|
151
|
-
_livePrices = livePrices;
|
|
152
|
-
_accountTotalValue = newTotalValue;
|
|
153
|
-
_isFetchingPrices = false;
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
Map<String, dynamic> _calculatePortfolioSummary(
|
|
158
|
-
List<Map<String, dynamic>> trades, List<Map<String, dynamic>> dividends) {
|
|
159
|
-
Map<String, dynamic> summary = {};
|
|
160
|
-
double accountTotalBookCost = 0;
|
|
161
|
-
|
|
162
|
-
Map<String, double> dividendSummary = {};
|
|
163
|
-
if (widget.showDividends) {
|
|
164
|
-
for (var dividend in dividends) {
|
|
165
|
-
final asset = dividend['asset'];
|
|
166
|
-
if (asset != null && asset['symbol'] != null) {
|
|
167
|
-
String symbol = asset['symbol'];
|
|
168
|
-
double amount = double.parse(dividend['amount'].toString());
|
|
169
|
-
dividendSummary.update(symbol, (value) => value + amount,
|
|
170
|
-
ifAbsent: () => amount);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
for (var trade in trades) {
|
|
176
|
-
final asset = trade['asset'];
|
|
177
|
-
if (asset == null || asset['symbol'] == null) continue;
|
|
178
|
-
String symbol = asset['symbol'];
|
|
179
|
-
|
|
180
|
-
double shares = double.parse(trade['shares'].toString());
|
|
181
|
-
double price = double.parse(trade['price'].toString());
|
|
182
|
-
String tradeType = trade['trade_type'];
|
|
183
|
-
DateTime tradeDate = DateTime.parse(trade['date']);
|
|
184
|
-
|
|
185
|
-
if (!summary.containsKey(symbol)) {
|
|
186
|
-
summary[symbol] = {
|
|
187
|
-
'symbol': symbol,
|
|
188
|
-
'name': asset['name'] ?? symbol,
|
|
189
|
-
'industry': asset['industry'] ?? 'N/A',
|
|
190
|
-
'shares': 0.0,
|
|
191
|
-
'book_cost': 0.0, // Renamed from total_cost for clarity
|
|
192
|
-
'total_dividends': dividendSummary[symbol] ?? 0.0,
|
|
193
|
-
'first_trade_date': tradeDate,
|
|
194
|
-
'last_activity_date': tradeDate,
|
|
195
|
-
};
|
|
196
|
-
} else {
|
|
197
|
-
if (tradeDate.isBefore(summary[symbol]['first_trade_date'])) {
|
|
198
|
-
summary[symbol]['first_trade_date'] = tradeDate;
|
|
199
|
-
}
|
|
200
|
-
if (tradeDate.isAfter(summary[symbol]['last_activity_date'])) {
|
|
201
|
-
summary[symbol]['last_activity_date'] = tradeDate;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (tradeType == 'buy') {
|
|
206
|
-
summary[symbol]['shares'] += shares;
|
|
207
|
-
summary[symbol]['book_cost'] += shares * price;
|
|
208
|
-
} else if (tradeType == 'sell') {
|
|
209
|
-
double originalShares = summary[symbol]['shares'];
|
|
210
|
-
if (originalShares > 0) {
|
|
211
|
-
double avgPrice = summary[symbol]['book_cost'] / originalShares;
|
|
212
|
-
summary[symbol]['book_cost'] -= shares * avgPrice;
|
|
213
|
-
}
|
|
214
|
-
summary[symbol]['shares'] -= shares;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
summary.removeWhere((key, value) => value['shares'] < 0.01);
|
|
219
|
-
|
|
220
|
-
accountTotalBookCost =
|
|
221
|
-
summary.values.fold(0.0, (sum, item) => sum + item['book_cost']);
|
|
222
|
-
|
|
223
|
-
List<Map<String, dynamic>> result = [];
|
|
224
|
-
summary.forEach((symbol, data) {
|
|
225
|
-
double shares = data['shares'];
|
|
226
|
-
double bookCost = data['book_cost'];
|
|
227
|
-
double totalDividends = data['total_dividends'];
|
|
228
|
-
|
|
229
|
-
data['avg_price'] = (shares > 0) ? bookCost / shares : 0.0;
|
|
230
|
-
data['account_allocation_percent'] = (accountTotalBookCost > 0)
|
|
231
|
-
? (bookCost / accountTotalBookCost) * 100
|
|
232
|
-
: 0.0;
|
|
233
|
-
data['portfolio_allocation_percent'] = (_totalPortfolioBookCost > 0)
|
|
234
|
-
? (bookCost / _totalPortfolioBookCost) * 100
|
|
235
|
-
: 0.0;
|
|
236
|
-
|
|
237
|
-
// Calculate Market Value, Unrealized P/L, Total Return
|
|
238
|
-
final livePrice = _livePrices[symbol];
|
|
239
|
-
double marketValue = livePrice != null ? shares * livePrice : bookCost; // Fallback to bookCost if no live price
|
|
240
|
-
double unrealizedPL = marketValue - bookCost;
|
|
241
|
-
double totalReturn = unrealizedPL + totalDividends;
|
|
242
|
-
|
|
243
|
-
data['market_value'] = marketValue;
|
|
244
|
-
data['unrealized_pl'] = unrealizedPL;
|
|
245
|
-
data['total_return'] = totalReturn;
|
|
246
|
-
|
|
247
|
-
data['percent_unrealized_pl'] = (bookCost > 0) ? (unrealizedPL / bookCost) * 100 : 0.0;
|
|
248
|
-
data['percent_total_return'] = (bookCost > 0) ? (totalReturn / bookCost) * 100 : 0.0;
|
|
249
|
-
|
|
250
|
-
result.add(data);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
return {'summary': result, 'total_value': accountTotalBookCost};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
@override
|
|
257
|
-
Widget build(BuildContext context) {
|
|
258
|
-
return Scaffold(
|
|
259
|
-
appBar: AppBar(
|
|
260
|
-
title: Text('${widget.accountName} Portfolio'),
|
|
261
|
-
actions: [
|
|
262
|
-
Center(
|
|
263
|
-
child: Padding(
|
|
264
|
-
padding: const EdgeInsets.only(right: 16.0),
|
|
265
|
-
child: Column(
|
|
266
|
-
mainAxisAlignment: MainAxisAlignment.center,
|
|
267
|
-
crossAxisAlignment: CrossAxisAlignment.end,
|
|
268
|
-
children: [
|
|
269
|
-
Text(
|
|
270
|
-
'Total Portfolio: ${_formatCurrency(_accountTotalValue)}',
|
|
271
|
-
style: const TextStyle(
|
|
272
|
-
fontSize: 16, fontWeight: FontWeight.bold)),
|
|
273
|
-
Text('Account Balance: ${_formatCurrency(_cashBalance)}',
|
|
274
|
-
style: const TextStyle(fontSize: 12)),
|
|
275
|
-
],
|
|
276
|
-
),
|
|
277
|
-
),
|
|
278
|
-
),
|
|
279
|
-
IconButton(
|
|
280
|
-
icon: const Icon(Icons.refresh),
|
|
281
|
-
onPressed: _fetchData,
|
|
282
|
-
tooltip: 'Refresh Data'),
|
|
283
|
-
],
|
|
284
|
-
),
|
|
285
|
-
body: _isLoading
|
|
286
|
-
? const Center(child: CircularProgressIndicator())
|
|
287
|
-
: RefreshIndicator(
|
|
288
|
-
onRefresh: _fetchData,
|
|
289
|
-
child: SingleChildScrollView(
|
|
290
|
-
physics: const AlwaysScrollableScrollPhysics(),
|
|
291
|
-
padding: const EdgeInsets.all(16.0),
|
|
292
|
-
child: Column(
|
|
293
|
-
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
294
|
-
children: [
|
|
295
|
-
Row(
|
|
296
|
-
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
297
|
-
children: [
|
|
298
|
-
Text('Portfolio Summary (Sintético)',
|
|
299
|
-
style: Theme.of(context).textTheme.titleLarge),
|
|
300
|
-
if (_isFetchingPrices)
|
|
301
|
-
const SizedBox(
|
|
302
|
-
height: 20,
|
|
303
|
-
width: 20,
|
|
304
|
-
child:
|
|
305
|
-
CircularProgressIndicator(strokeWidth: 2.0)),
|
|
306
|
-
],
|
|
307
|
-
),
|
|
308
|
-
_buildSinteticoTable(),
|
|
309
|
-
const SizedBox(height: 24),
|
|
310
|
-
Row(
|
|
311
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
312
|
-
children: [
|
|
313
|
-
Expanded(
|
|
314
|
-
child: TradeForm(
|
|
315
|
-
accountName: widget.accountName,
|
|
316
|
-
portfolioSummary: _portfolioSummary,
|
|
317
|
-
assets: _assets,
|
|
318
|
-
onTradeCreated: (_) => _fetchData(),
|
|
319
|
-
token: _token,
|
|
320
|
-
)),
|
|
321
|
-
if (widget.showDividends) ...[
|
|
322
|
-
const SizedBox(width: 16),
|
|
323
|
-
Expanded(
|
|
324
|
-
child: DividendLogForm(
|
|
325
|
-
investmentAccount: widget.accountName,
|
|
326
|
-
assets: _assets,
|
|
327
|
-
onDividendLogged: () => _fetchData(),
|
|
328
|
-
token: _token,
|
|
329
|
-
),
|
|
330
|
-
),
|
|
331
|
-
],
|
|
332
|
-
],
|
|
333
|
-
),
|
|
334
|
-
const SizedBox(height: 24),
|
|
335
|
-
Text('Trade History (Analítico)',
|
|
336
|
-
style: Theme.of(context).textTheme.titleLarge),
|
|
337
|
-
const SizedBox(height: 8),
|
|
338
|
-
_buildAnaliticoFilter(),
|
|
339
|
-
const SizedBox(height: 8),
|
|
340
|
-
_buildAnaliticoTable(),
|
|
341
|
-
],
|
|
342
|
-
),
|
|
343
|
-
),
|
|
344
|
-
),
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
Widget _buildAnaliticoFilter() {
|
|
349
|
-
final symbols =
|
|
350
|
-
_portfolioSummary.map((p) => p['symbol'] as String).toSet().toList();
|
|
351
158
|
return Padding(
|
|
352
159
|
padding: const EdgeInsets.only(bottom: 8.0),
|
|
353
160
|
child: AppDropdown<String?>(
|
|
354
|
-
value:
|
|
161
|
+
value: viewModel.selectedSymbolForFilter,
|
|
355
162
|
hint: 'Filter by Symbol',
|
|
356
163
|
items: [
|
|
357
164
|
const DropdownMenuItem<String?>(
|
|
@@ -359,18 +166,21 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
359
166
|
...symbols.map((symbol) =>
|
|
360
167
|
DropdownMenuItem<String?>(value: symbol, child: Text(symbol))),
|
|
361
168
|
],
|
|
362
|
-
onChanged: (String? newValue) =>
|
|
363
|
-
setState(() => _selectedSymbolForFilter = newValue),
|
|
169
|
+
onChanged: (String? newValue) => viewModel.setSymbolFilter(newValue),
|
|
364
170
|
),
|
|
365
171
|
);
|
|
366
172
|
}
|
|
367
173
|
|
|
368
|
-
Widget _buildSinteticoTable(
|
|
369
|
-
|
|
174
|
+
Widget _buildSinteticoTable(
|
|
175
|
+
BuildContext context, InvestmentAccountViewModel viewModel) {
|
|
176
|
+
if (viewModel.portfolioSummary.isEmpty) {
|
|
370
177
|
return const Center(
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
178
|
+
child: Padding(
|
|
179
|
+
padding: EdgeInsets.all(16.0),
|
|
180
|
+
child: Text('No positions held.'),
|
|
181
|
+
),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
374
184
|
|
|
375
185
|
return SingleChildScrollView(
|
|
376
186
|
scrollDirection: Axis.horizontal,
|
|
@@ -385,7 +195,7 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
385
195
|
const DataColumn(label: Text('Market Value')),
|
|
386
196
|
const DataColumn(label: Text('Unrealized P/L')),
|
|
387
197
|
const DataColumn(label: Text('% P/L')),
|
|
388
|
-
if (
|
|
198
|
+
if (showDividends) ...[
|
|
389
199
|
const DataColumn(label: Text('Dividends')),
|
|
390
200
|
const DataColumn(label: Text('Total Return')),
|
|
391
201
|
const DataColumn(label: Text('% Total Return')),
|
|
@@ -393,26 +203,27 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
393
203
|
const DataColumn(label: Text('Account %')),
|
|
394
204
|
const DataColumn(label: Text('Portfolio %')),
|
|
395
205
|
],
|
|
396
|
-
rows:
|
|
206
|
+
rows: viewModel.portfolioSummary.map((position) {
|
|
397
207
|
final symbol = position['symbol'] as String;
|
|
398
208
|
final shares = position['shares'] as double;
|
|
399
209
|
final avgPrice = position['avg_price'] as double;
|
|
400
210
|
final bookCost = position['book_cost'] as double;
|
|
401
|
-
final livePrice =
|
|
211
|
+
final livePrice = viewModel.livePrices[symbol];
|
|
402
212
|
final marketValue = position['market_value'] as double;
|
|
403
213
|
final unrealizedPL = position['unrealized_pl'] as double;
|
|
404
|
-
final percentUnrealizedPL =
|
|
214
|
+
final percentUnrealizedPL =
|
|
215
|
+
position['percent_unrealized_pl'] as double;
|
|
405
216
|
final totalDividends = position['total_dividends'] as double;
|
|
406
217
|
final totalReturn = position['total_return'] as double;
|
|
407
|
-
final percentTotalReturn =
|
|
408
|
-
|
|
409
|
-
final
|
|
218
|
+
final percentTotalReturn =
|
|
219
|
+
position['percent_total_return'] as double;
|
|
220
|
+
final accountAllocationPercent =
|
|
221
|
+
position['account_allocation_percent'] as double;
|
|
222
|
+
final portfolioAllocationPercent =
|
|
223
|
+
position['portfolio_allocation_percent'] as double;
|
|
410
224
|
|
|
411
|
-
|
|
412
|
-
final
|
|
413
|
-
(unrealizedPL ?? 0) >= 0 ? Colors.green : Colors.red;
|
|
414
|
-
final totalReturnColor =
|
|
415
|
-
(totalReturn ?? 0) >= 0 ? Colors.green : Colors.red;
|
|
225
|
+
final plColor = unrealizedPL >= 0 ? Colors.green : Colors.red;
|
|
226
|
+
final totalReturnColor = totalReturn >= 0 ? Colors.green : Colors.red;
|
|
416
227
|
|
|
417
228
|
return DataRow(cells: [
|
|
418
229
|
DataCell(Text(symbol)),
|
|
@@ -423,12 +234,17 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
423
234
|
? Text(_formatCurrency(livePrice))
|
|
424
235
|
: const Text('N/A')),
|
|
425
236
|
DataCell(Text(_formatCurrency(marketValue))),
|
|
426
|
-
DataCell(
|
|
427
|
-
|
|
428
|
-
|
|
237
|
+
DataCell(
|
|
238
|
+
Text(_formatCurrency(unrealizedPL),
|
|
239
|
+
style: TextStyle(color: plColor))),
|
|
240
|
+
DataCell(Text('${percentUnrealizedPL.toStringAsFixed(2)}%',
|
|
241
|
+
style: TextStyle(color: plColor))),
|
|
242
|
+
if (showDividends) ...[
|
|
429
243
|
DataCell(Text(_formatCurrency(totalDividends))),
|
|
430
|
-
DataCell(Text(_formatCurrency(totalReturn),
|
|
431
|
-
|
|
244
|
+
DataCell(Text(_formatCurrency(totalReturn),
|
|
245
|
+
style: TextStyle(color: totalReturnColor))),
|
|
246
|
+
DataCell(Text('${percentTotalReturn.toStringAsFixed(2)}%',
|
|
247
|
+
style: TextStyle(color: totalReturnColor))),
|
|
432
248
|
],
|
|
433
249
|
DataCell(Text('${accountAllocationPercent.toStringAsFixed(2)}%')),
|
|
434
250
|
DataCell(Text('${portfolioAllocationPercent.toStringAsFixed(2)}%')),
|
|
@@ -438,37 +254,40 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
438
254
|
);
|
|
439
255
|
}
|
|
440
256
|
|
|
441
|
-
Widget _buildAnaliticoTable(
|
|
442
|
-
|
|
257
|
+
Widget _buildAnaliticoTable(
|
|
258
|
+
BuildContext context, InvestmentAccountViewModel viewModel) {
|
|
259
|
+
if (viewModel.trades.isEmpty) {
|
|
443
260
|
return const Center(
|
|
444
|
-
|
|
445
|
-
|
|
261
|
+
child: Padding(
|
|
262
|
+
padding: EdgeInsets.all(16.0),
|
|
263
|
+
child: Text('No trades found.'),
|
|
264
|
+
),
|
|
265
|
+
);
|
|
266
|
+
}
|
|
446
267
|
|
|
447
268
|
final List<Map<String, dynamic>> filteredTrades =
|
|
448
|
-
|
|
449
|
-
?
|
|
450
|
-
:
|
|
269
|
+
viewModel.selectedSymbolForFilter == null
|
|
270
|
+
? viewModel.trades
|
|
271
|
+
: viewModel.trades
|
|
451
272
|
.where((trade) =>
|
|
452
|
-
trade['asset']?['symbol'] ==
|
|
273
|
+
trade['asset']?['symbol'] ==
|
|
274
|
+
viewModel.selectedSymbolForFilter)
|
|
453
275
|
.toList();
|
|
454
276
|
|
|
455
|
-
if (filteredTrades.isEmpty)
|
|
277
|
+
if (filteredTrades.isEmpty) {
|
|
456
278
|
return const Center(
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
279
|
+
child: Padding(
|
|
280
|
+
padding: EdgeInsets.all(16.0),
|
|
281
|
+
child: Text('No trades match the selected symbol.'),
|
|
282
|
+
),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
460
285
|
|
|
461
286
|
final sortedTrades = List<Map<String, dynamic>>.from(filteredTrades)
|
|
462
287
|
..sort((a, b) {
|
|
463
|
-
final
|
|
464
|
-
final
|
|
465
|
-
|
|
466
|
-
try {
|
|
467
|
-
return DateTime.parse(b['date'] as String)
|
|
468
|
-
.compareTo(DateTime.parse(a['date'] as String));
|
|
469
|
-
} catch (e) {
|
|
470
|
-
return 0;
|
|
471
|
-
}
|
|
288
|
+
final dateA = DateTime.parse(a['date'] as String);
|
|
289
|
+
final dateB = DateTime.parse(b['date'] as String);
|
|
290
|
+
return dateB.compareTo(dateA);
|
|
472
291
|
});
|
|
473
292
|
|
|
474
293
|
return DataTable(
|
|
@@ -484,7 +303,7 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
484
303
|
DataColumn(label: Text('Return')),
|
|
485
304
|
DataColumn(label: Text('% Return')),
|
|
486
305
|
DataColumn(label: Text('Book Cost')),
|
|
487
|
-
DataColumn(label: Text('Actions')),
|
|
306
|
+
DataColumn(label: Text('Actions')),
|
|
488
307
|
],
|
|
489
308
|
rows: sortedTrades.map((trade) {
|
|
490
309
|
final double shares = double.parse(trade['shares'].toString());
|
|
@@ -493,8 +312,8 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
493
312
|
final tradeDate = DateTime.parse(trade['date'] as String);
|
|
494
313
|
final tradeAge = DateTime.now().difference(tradeDate);
|
|
495
314
|
final symbol = trade['asset']?['symbol'] ?? '';
|
|
496
|
-
final livePrice =
|
|
497
|
-
final int tradeId = trade['id'] as int;
|
|
315
|
+
final livePrice = viewModel.livePrices[symbol];
|
|
316
|
+
final int tradeId = trade['id'] as int;
|
|
498
317
|
|
|
499
318
|
double? tradeReturnValue, tradePercentageReturn, tradeBookCost;
|
|
500
319
|
|
|
@@ -502,8 +321,9 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
502
321
|
final currentMarketValue = livePrice * shares;
|
|
503
322
|
tradeReturnValue = currentMarketValue - total;
|
|
504
323
|
tradeBookCost = currentMarketValue;
|
|
505
|
-
if (total > 0)
|
|
324
|
+
if (total > 0) {
|
|
506
325
|
tradePercentageReturn = (tradeReturnValue / total) * 100;
|
|
326
|
+
}
|
|
507
327
|
}
|
|
508
328
|
|
|
509
329
|
final returnColor =
|
|
@@ -520,22 +340,28 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
520
340
|
? Text(_formatCurrency(livePrice))
|
|
521
341
|
: const Text('N/A')),
|
|
522
342
|
DataCell(Text(_formatCurrency(total))),
|
|
523
|
-
DataCell(
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
343
|
+
DataCell(
|
|
344
|
+
tradeReturnValue != null
|
|
345
|
+
? Text(_formatCurrency(tradeReturnValue),
|
|
346
|
+
style: TextStyle(color: returnColor))
|
|
347
|
+
: const Text('N/A'),
|
|
348
|
+
),
|
|
349
|
+
DataCell(
|
|
350
|
+
tradePercentageReturn != null
|
|
351
|
+
? Text('${tradePercentageReturn.toStringAsFixed(2)}%',
|
|
352
|
+
style: TextStyle(color: returnColor))
|
|
353
|
+
: const Text('N/A'),
|
|
354
|
+
),
|
|
355
|
+
DataCell(
|
|
356
|
+
tradeBookCost != null
|
|
357
|
+
? Text(_formatCurrency(tradeBookCost),
|
|
358
|
+
style: TextStyle(color: returnColor))
|
|
359
|
+
: const Text('N/A'),
|
|
360
|
+
),
|
|
535
361
|
DataCell(
|
|
536
362
|
IconButton(
|
|
537
363
|
icon: const Icon(Icons.delete, color: Colors.red),
|
|
538
|
-
onPressed: () => _confirmAndDeleteTrade(tradeId),
|
|
364
|
+
onPressed: () => _confirmAndDeleteTrade(context, viewModel, tradeId),
|
|
539
365
|
tooltip: 'Delete Trade',
|
|
540
366
|
),
|
|
541
367
|
),
|
|
@@ -543,32 +369,21 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
543
369
|
}).toList(),
|
|
544
370
|
);
|
|
545
371
|
}
|
|
546
|
-
|
|
547
|
-
Future<void> _confirmAndDeleteTrade(
|
|
548
|
-
|
|
372
|
+
|
|
373
|
+
Future<void> _confirmAndDeleteTrade(BuildContext context,
|
|
374
|
+
InvestmentAccountViewModel viewModel, int tradeId) async {
|
|
375
|
+
final bool? confirm =
|
|
376
|
+
await AppDialogs.showDeleteConfirmationDialog(context, 'this trade');
|
|
549
377
|
|
|
550
378
|
if (confirm == true) {
|
|
551
|
-
|
|
379
|
+
final success = await viewModel.deleteTrade(tradeId);
|
|
380
|
+
if (ScaffoldMessenger.of(context).mounted) {
|
|
552
381
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
final success = await _transactionService.deleteTrade(tradeId, token: _token);
|
|
559
|
-
if (success) {
|
|
560
|
-
ScaffoldMessenger.of(context).showSnackBar(
|
|
561
|
-
const SnackBar(content: Text('Trade deleted successfully.')),
|
|
562
|
-
);
|
|
563
|
-
_fetchData(); // Refresh data after deletion
|
|
564
|
-
} else {
|
|
565
|
-
ScaffoldMessenger.of(context).showSnackBar(
|
|
566
|
-
const SnackBar(content: Text('Failed to delete trade.')),
|
|
567
|
-
);
|
|
568
|
-
}
|
|
569
|
-
} catch (e) {
|
|
570
|
-
ScaffoldMessenger.of(context).showSnackBar(
|
|
571
|
-
SnackBar(content: Text('Error deleting trade: $e')),
|
|
382
|
+
SnackBar(
|
|
383
|
+
content: Text(success
|
|
384
|
+
? 'Trade deleted successfully.'
|
|
385
|
+
: 'Failed to delete trade.'),
|
|
386
|
+
),
|
|
572
387
|
);
|
|
573
388
|
}
|
|
574
389
|
}
|