@marcos_feitoza/personal-finance-frontend-feature-investments 1.1.1 → 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 +7 -0
- package/lib/screens/crypto_account_screen.dart +169 -262
- package/lib/screens/investment_account_screen.dart +202 -401
- package/lib/screens/rrsp_sun_life_screen.dart +117 -211
- 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,294 +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(
|
|
80
|
-
investmentAccount: widget.accountName, token: _token),
|
|
81
|
-
_transactionService.getAccountBalance(widget.accountName,
|
|
82
|
-
token: _token),
|
|
83
|
-
_transactionService.getAssets(
|
|
84
|
-
investmentAccount: widget.accountName, token: _token),
|
|
85
|
-
_transactionService.getTotalPortfolioBookCost(token: _token),
|
|
86
|
-
];
|
|
87
|
-
if (widget.showDividends) {
|
|
88
|
-
futures.add(_transactionService.getDividends(
|
|
89
|
-
investmentAccount: widget.accountName, token: _token));
|
|
90
|
-
}
|
|
91
|
-
final results = await Future.wait(futures);
|
|
92
|
-
|
|
93
|
-
final trades = results[0] as List<Map<String, dynamic>>;
|
|
94
|
-
final balance = results[1] as double;
|
|
95
|
-
final assets = results[2] as List<Map<String, dynamic>>;
|
|
96
|
-
final totalPortfolioBookCost = results[3] as double;
|
|
97
|
-
final dividends = widget.showDividends
|
|
98
|
-
? results[4] as List<Map<String, dynamic>>
|
|
99
|
-
: <Map<String, dynamic>>[];
|
|
100
|
-
|
|
101
|
-
final summaryData = _calculatePortfolioSummary(trades, dividends);
|
|
102
|
-
|
|
103
|
-
setState(() {
|
|
104
|
-
_trades = trades;
|
|
105
|
-
_dividends = dividends;
|
|
106
|
-
_cashBalance = balance;
|
|
107
|
-
_assets = assets;
|
|
108
|
-
_portfolioSummary = summaryData['summary'];
|
|
109
|
-
_accountTotalValue = summaryData['total_value'] + _cashBalance;
|
|
110
|
-
_totalPortfolioBookCost = totalPortfolioBookCost;
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
if (mounted) await _fetchLivePrices();
|
|
114
|
-
} catch (e) {
|
|
115
|
-
if (mounted)
|
|
116
|
-
ScaffoldMessenger.of(context)
|
|
117
|
-
.showSnackBar(SnackBar(content: Text('Error fetching data: $e')));
|
|
118
|
-
} finally {
|
|
119
|
-
if (mounted) setState(() => _isLoading = false);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
Future<void> _fetchLivePrices() async {
|
|
124
|
-
if (_portfolioSummary.isEmpty || !mounted) return;
|
|
125
|
-
setState(() => _isFetchingPrices = true);
|
|
126
|
-
|
|
127
|
-
final symbolsToFetch = _portfolioSummary
|
|
152
|
+
Widget _buildAnaliticoFilter(
|
|
153
|
+
BuildContext context, InvestmentAccountViewModel viewModel) {
|
|
154
|
+
final symbols = viewModel.portfolioSummary
|
|
128
155
|
.map((p) => p['symbol'] as String)
|
|
129
|
-
.
|
|
156
|
+
.toSet()
|
|
130
157
|
.toList();
|
|
131
|
-
|
|
132
|
-
if (symbolsToFetch.isEmpty) {
|
|
133
|
-
setState(() => _isFetchingPrices = false);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
final livePrices = await _stockService.getLivePrices(symbolsToFetch);
|
|
138
|
-
|
|
139
|
-
if (!mounted) return;
|
|
140
|
-
|
|
141
|
-
double newTotalValue = _cashBalance;
|
|
142
|
-
for (final position in _portfolioSummary) {
|
|
143
|
-
final shares = double.parse(position['shares'].toString());
|
|
144
|
-
final livePrice = livePrices[position['symbol']];
|
|
145
|
-
if (livePrice != null) {
|
|
146
|
-
newTotalValue += shares * livePrice;
|
|
147
|
-
} else {
|
|
148
|
-
// If live price is not available, use book cost as a fallback for market value
|
|
149
|
-
newTotalValue += (position['book_cost'] as num?)?.toDouble() ?? 0.0;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
setState(() {
|
|
154
|
-
_livePrices = livePrices;
|
|
155
|
-
_accountTotalValue = newTotalValue;
|
|
156
|
-
_isFetchingPrices = false;
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
Map<String, dynamic> _calculatePortfolioSummary(
|
|
161
|
-
List<Map<String, dynamic>> trades, List<Map<String, dynamic>> dividends) {
|
|
162
|
-
Map<String, dynamic> summary = {};
|
|
163
|
-
double accountTotalBookCost = 0;
|
|
164
|
-
|
|
165
|
-
Map<String, double> dividendSummary = {};
|
|
166
|
-
if (widget.showDividends) {
|
|
167
|
-
for (var dividend in dividends) {
|
|
168
|
-
final asset = dividend['asset'];
|
|
169
|
-
if (asset != null && asset['symbol'] != null) {
|
|
170
|
-
String symbol = asset['symbol'];
|
|
171
|
-
double amount = double.parse(dividend['amount'].toString());
|
|
172
|
-
dividendSummary.update(symbol, (value) => value + amount,
|
|
173
|
-
ifAbsent: () => amount);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
for (var trade in trades) {
|
|
179
|
-
final asset = trade['asset'];
|
|
180
|
-
if (asset == null || asset['symbol'] == null) continue;
|
|
181
|
-
String symbol = asset['symbol'];
|
|
182
|
-
|
|
183
|
-
double shares = double.parse(trade['shares'].toString());
|
|
184
|
-
double price = double.parse(trade['price'].toString());
|
|
185
|
-
String tradeType = trade['trade_type'];
|
|
186
|
-
DateTime tradeDate = DateTime.parse(trade['date']);
|
|
187
|
-
|
|
188
|
-
if (!summary.containsKey(symbol)) {
|
|
189
|
-
summary[symbol] = {
|
|
190
|
-
'symbol': symbol,
|
|
191
|
-
'name': asset['name'] ?? symbol,
|
|
192
|
-
'industry': asset['industry'] ?? 'N/A',
|
|
193
|
-
'shares': 0.0,
|
|
194
|
-
'book_cost': 0.0, // Renamed from total_cost for clarity
|
|
195
|
-
'total_dividends': dividendSummary[symbol] ?? 0.0,
|
|
196
|
-
'first_trade_date': tradeDate,
|
|
197
|
-
'last_activity_date': tradeDate,
|
|
198
|
-
};
|
|
199
|
-
} else {
|
|
200
|
-
if (tradeDate.isBefore(summary[symbol]['first_trade_date'])) {
|
|
201
|
-
summary[symbol]['first_trade_date'] = tradeDate;
|
|
202
|
-
}
|
|
203
|
-
if (tradeDate.isAfter(summary[symbol]['last_activity_date'])) {
|
|
204
|
-
summary[symbol]['last_activity_date'] = tradeDate;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (tradeType == 'buy') {
|
|
209
|
-
summary[symbol]['shares'] += shares;
|
|
210
|
-
summary[symbol]['book_cost'] += shares * price;
|
|
211
|
-
} else if (tradeType == 'sell') {
|
|
212
|
-
double originalShares = summary[symbol]['shares'];
|
|
213
|
-
if (originalShares > 0) {
|
|
214
|
-
double avgPrice = summary[symbol]['book_cost'] / originalShares;
|
|
215
|
-
summary[symbol]['book_cost'] -= shares * avgPrice;
|
|
216
|
-
}
|
|
217
|
-
summary[symbol]['shares'] -= shares;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
summary.removeWhere((key, value) => value['shares'] < 0.01);
|
|
222
|
-
|
|
223
|
-
accountTotalBookCost =
|
|
224
|
-
summary.values.fold(0.0, (sum, item) => sum + item['book_cost']);
|
|
225
|
-
|
|
226
|
-
List<Map<String, dynamic>> result = [];
|
|
227
|
-
summary.forEach((symbol, data) {
|
|
228
|
-
double shares = data['shares'];
|
|
229
|
-
double bookCost = data['book_cost'];
|
|
230
|
-
double totalDividends = data['total_dividends'];
|
|
231
|
-
|
|
232
|
-
data['avg_price'] = (shares > 0) ? bookCost / shares : 0.0;
|
|
233
|
-
data['account_allocation_percent'] = (accountTotalBookCost > 0)
|
|
234
|
-
? (bookCost / accountTotalBookCost) * 100
|
|
235
|
-
: 0.0;
|
|
236
|
-
data['portfolio_allocation_percent'] = (_totalPortfolioBookCost > 0)
|
|
237
|
-
? (bookCost / _totalPortfolioBookCost) * 100
|
|
238
|
-
: 0.0;
|
|
239
|
-
|
|
240
|
-
// Calculate Market Value, Unrealized P/L, Total Return
|
|
241
|
-
final livePrice = _livePrices[symbol];
|
|
242
|
-
double marketValue = livePrice != null
|
|
243
|
-
? shares * livePrice
|
|
244
|
-
: bookCost; // Fallback to bookCost if no live price
|
|
245
|
-
double unrealizedPL = marketValue - bookCost;
|
|
246
|
-
double totalReturn = unrealizedPL + totalDividends;
|
|
247
|
-
|
|
248
|
-
data['market_value'] = marketValue;
|
|
249
|
-
data['unrealized_pl'] = unrealizedPL;
|
|
250
|
-
data['total_return'] = totalReturn;
|
|
251
|
-
|
|
252
|
-
data['percent_unrealized_pl'] =
|
|
253
|
-
(bookCost > 0) ? (unrealizedPL / bookCost) * 100 : 0.0;
|
|
254
|
-
data['percent_total_return'] =
|
|
255
|
-
(bookCost > 0) ? (totalReturn / bookCost) * 100 : 0.0;
|
|
256
|
-
|
|
257
|
-
result.add(data);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
return {'summary': result, 'total_value': accountTotalBookCost};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
@override
|
|
264
|
-
Widget build(BuildContext context) {
|
|
265
|
-
return Scaffold(
|
|
266
|
-
appBar: AppBar(
|
|
267
|
-
title: Text('${widget.accountName} Portfolio'),
|
|
268
|
-
actions: [
|
|
269
|
-
Center(
|
|
270
|
-
child: Padding(
|
|
271
|
-
padding: const EdgeInsets.only(right: 16.0),
|
|
272
|
-
child: Column(
|
|
273
|
-
mainAxisAlignment: MainAxisAlignment.center,
|
|
274
|
-
crossAxisAlignment: CrossAxisAlignment.end,
|
|
275
|
-
children: [
|
|
276
|
-
Text(
|
|
277
|
-
'Total Portfolio: ${_formatCurrency(_accountTotalValue)}',
|
|
278
|
-
style: const TextStyle(
|
|
279
|
-
fontSize: 16, fontWeight: FontWeight.bold)),
|
|
280
|
-
Text('Account Balance: ${_formatCurrency(_cashBalance)}',
|
|
281
|
-
style: const TextStyle(fontSize: 12)),
|
|
282
|
-
],
|
|
283
|
-
),
|
|
284
|
-
),
|
|
285
|
-
),
|
|
286
|
-
IconButton(
|
|
287
|
-
icon: const Icon(Icons.refresh),
|
|
288
|
-
onPressed: _fetchData,
|
|
289
|
-
tooltip: 'Refresh Data'),
|
|
290
|
-
],
|
|
291
|
-
),
|
|
292
|
-
body: _isLoading
|
|
293
|
-
? const Center(child: CircularProgressIndicator())
|
|
294
|
-
: RefreshIndicator(
|
|
295
|
-
onRefresh: _fetchData,
|
|
296
|
-
child: SingleChildScrollView(
|
|
297
|
-
physics: const AlwaysScrollableScrollPhysics(),
|
|
298
|
-
padding: const EdgeInsets.all(16.0),
|
|
299
|
-
child: Column(
|
|
300
|
-
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
301
|
-
children: [
|
|
302
|
-
Row(
|
|
303
|
-
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
304
|
-
children: [
|
|
305
|
-
Text('Portfolio Summary (Sintético)',
|
|
306
|
-
style: Theme.of(context).textTheme.titleLarge),
|
|
307
|
-
if (_isFetchingPrices)
|
|
308
|
-
const SizedBox(
|
|
309
|
-
height: 20,
|
|
310
|
-
width: 20,
|
|
311
|
-
child:
|
|
312
|
-
CircularProgressIndicator(strokeWidth: 2.0)),
|
|
313
|
-
],
|
|
314
|
-
),
|
|
315
|
-
_buildSinteticoTable(),
|
|
316
|
-
const SizedBox(height: 24),
|
|
317
|
-
Row(
|
|
318
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
319
|
-
children: [
|
|
320
|
-
Expanded(
|
|
321
|
-
child: TradeForm(
|
|
322
|
-
accountName: widget.accountName,
|
|
323
|
-
portfolioSummary: _portfolioSummary,
|
|
324
|
-
assets: _assets,
|
|
325
|
-
onTradeCreated: (_) => _fetchData(),
|
|
326
|
-
token: _token,
|
|
327
|
-
)),
|
|
328
|
-
if (widget.showDividends) ...[
|
|
329
|
-
const SizedBox(width: 16),
|
|
330
|
-
Expanded(
|
|
331
|
-
child: DividendLogForm(
|
|
332
|
-
investmentAccount: widget.accountName,
|
|
333
|
-
assets: _assets,
|
|
334
|
-
onDividendLogged: () => _fetchData(),
|
|
335
|
-
token: _token,
|
|
336
|
-
),
|
|
337
|
-
),
|
|
338
|
-
],
|
|
339
|
-
],
|
|
340
|
-
),
|
|
341
|
-
const SizedBox(height: 24),
|
|
342
|
-
Text('Trade History (Analítico)',
|
|
343
|
-
style: Theme.of(context).textTheme.titleLarge),
|
|
344
|
-
const SizedBox(height: 8),
|
|
345
|
-
_buildAnaliticoFilter(),
|
|
346
|
-
const SizedBox(height: 8),
|
|
347
|
-
_buildAnaliticoTable(),
|
|
348
|
-
],
|
|
349
|
-
),
|
|
350
|
-
),
|
|
351
|
-
),
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
Widget _buildAnaliticoFilter() {
|
|
356
|
-
final symbols =
|
|
357
|
-
_portfolioSummary.map((p) => p['symbol'] as String).toSet().toList();
|
|
358
158
|
return Padding(
|
|
359
159
|
padding: const EdgeInsets.only(bottom: 8.0),
|
|
360
160
|
child: AppDropdown<String?>(
|
|
361
|
-
value:
|
|
161
|
+
value: viewModel.selectedSymbolForFilter,
|
|
362
162
|
hint: 'Filter by Symbol',
|
|
363
163
|
items: [
|
|
364
164
|
const DropdownMenuItem<String?>(
|
|
@@ -366,18 +166,21 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
366
166
|
...symbols.map((symbol) =>
|
|
367
167
|
DropdownMenuItem<String?>(value: symbol, child: Text(symbol))),
|
|
368
168
|
],
|
|
369
|
-
onChanged: (String? newValue) =>
|
|
370
|
-
setState(() => _selectedSymbolForFilter = newValue),
|
|
169
|
+
onChanged: (String? newValue) => viewModel.setSymbolFilter(newValue),
|
|
371
170
|
),
|
|
372
171
|
);
|
|
373
172
|
}
|
|
374
173
|
|
|
375
|
-
Widget _buildSinteticoTable(
|
|
376
|
-
|
|
174
|
+
Widget _buildSinteticoTable(
|
|
175
|
+
BuildContext context, InvestmentAccountViewModel viewModel) {
|
|
176
|
+
if (viewModel.portfolioSummary.isEmpty) {
|
|
377
177
|
return const Center(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
178
|
+
child: Padding(
|
|
179
|
+
padding: EdgeInsets.all(16.0),
|
|
180
|
+
child: Text('No positions held.'),
|
|
181
|
+
),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
381
184
|
|
|
382
185
|
return SingleChildScrollView(
|
|
383
186
|
scrollDirection: Axis.horizontal,
|
|
@@ -392,7 +195,7 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
392
195
|
const DataColumn(label: Text('Market Value')),
|
|
393
196
|
const DataColumn(label: Text('Unrealized P/L')),
|
|
394
197
|
const DataColumn(label: Text('% P/L')),
|
|
395
|
-
if (
|
|
198
|
+
if (showDividends) ...[
|
|
396
199
|
const DataColumn(label: Text('Dividends')),
|
|
397
200
|
const DataColumn(label: Text('Total Return')),
|
|
398
201
|
const DataColumn(label: Text('% Total Return')),
|
|
@@ -400,27 +203,27 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
400
203
|
const DataColumn(label: Text('Account %')),
|
|
401
204
|
const DataColumn(label: Text('Portfolio %')),
|
|
402
205
|
],
|
|
403
|
-
rows:
|
|
206
|
+
rows: viewModel.portfolioSummary.map((position) {
|
|
404
207
|
final symbol = position['symbol'] as String;
|
|
405
208
|
final shares = position['shares'] as double;
|
|
406
209
|
final avgPrice = position['avg_price'] as double;
|
|
407
210
|
final bookCost = position['book_cost'] as double;
|
|
408
|
-
final livePrice =
|
|
211
|
+
final livePrice = viewModel.livePrices[symbol];
|
|
409
212
|
final marketValue = position['market_value'] as double;
|
|
410
213
|
final unrealizedPL = position['unrealized_pl'] as double;
|
|
411
214
|
final percentUnrealizedPL =
|
|
412
215
|
position['percent_unrealized_pl'] as double;
|
|
413
216
|
final totalDividends = position['total_dividends'] as double;
|
|
414
217
|
final totalReturn = position['total_return'] as double;
|
|
415
|
-
final percentTotalReturn =
|
|
218
|
+
final percentTotalReturn =
|
|
219
|
+
position['percent_total_return'] as double;
|
|
416
220
|
final accountAllocationPercent =
|
|
417
221
|
position['account_allocation_percent'] as double;
|
|
418
222
|
final portfolioAllocationPercent =
|
|
419
223
|
position['portfolio_allocation_percent'] as double;
|
|
420
224
|
|
|
421
|
-
final plColor =
|
|
422
|
-
final totalReturnColor =
|
|
423
|
-
(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;
|
|
424
227
|
|
|
425
228
|
return DataRow(cells: [
|
|
426
229
|
DataCell(Text(symbol)),
|
|
@@ -431,11 +234,12 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
431
234
|
? Text(_formatCurrency(livePrice))
|
|
432
235
|
: const Text('N/A')),
|
|
433
236
|
DataCell(Text(_formatCurrency(marketValue))),
|
|
434
|
-
DataCell(
|
|
435
|
-
|
|
237
|
+
DataCell(
|
|
238
|
+
Text(_formatCurrency(unrealizedPL),
|
|
239
|
+
style: TextStyle(color: plColor))),
|
|
436
240
|
DataCell(Text('${percentUnrealizedPL.toStringAsFixed(2)}%',
|
|
437
241
|
style: TextStyle(color: plColor))),
|
|
438
|
-
if (
|
|
242
|
+
if (showDividends) ...[
|
|
439
243
|
DataCell(Text(_formatCurrency(totalDividends))),
|
|
440
244
|
DataCell(Text(_formatCurrency(totalReturn),
|
|
441
245
|
style: TextStyle(color: totalReturnColor))),
|
|
@@ -450,37 +254,40 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
450
254
|
);
|
|
451
255
|
}
|
|
452
256
|
|
|
453
|
-
Widget _buildAnaliticoTable(
|
|
454
|
-
|
|
257
|
+
Widget _buildAnaliticoTable(
|
|
258
|
+
BuildContext context, InvestmentAccountViewModel viewModel) {
|
|
259
|
+
if (viewModel.trades.isEmpty) {
|
|
455
260
|
return const Center(
|
|
456
|
-
|
|
457
|
-
|
|
261
|
+
child: Padding(
|
|
262
|
+
padding: EdgeInsets.all(16.0),
|
|
263
|
+
child: Text('No trades found.'),
|
|
264
|
+
),
|
|
265
|
+
);
|
|
266
|
+
}
|
|
458
267
|
|
|
459
268
|
final List<Map<String, dynamic>> filteredTrades =
|
|
460
|
-
|
|
461
|
-
?
|
|
462
|
-
:
|
|
269
|
+
viewModel.selectedSymbolForFilter == null
|
|
270
|
+
? viewModel.trades
|
|
271
|
+
: viewModel.trades
|
|
463
272
|
.where((trade) =>
|
|
464
|
-
trade['asset']?['symbol'] ==
|
|
273
|
+
trade['asset']?['symbol'] ==
|
|
274
|
+
viewModel.selectedSymbolForFilter)
|
|
465
275
|
.toList();
|
|
466
276
|
|
|
467
|
-
if (filteredTrades.isEmpty)
|
|
277
|
+
if (filteredTrades.isEmpty) {
|
|
468
278
|
return const Center(
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
279
|
+
child: Padding(
|
|
280
|
+
padding: EdgeInsets.all(16.0),
|
|
281
|
+
child: Text('No trades match the selected symbol.'),
|
|
282
|
+
),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
472
285
|
|
|
473
286
|
final sortedTrades = List<Map<String, dynamic>>.from(filteredTrades)
|
|
474
287
|
..sort((a, b) {
|
|
475
|
-
final
|
|
476
|
-
final
|
|
477
|
-
|
|
478
|
-
try {
|
|
479
|
-
return DateTime.parse(b['date'] as String)
|
|
480
|
-
.compareTo(DateTime.parse(a['date'] as String));
|
|
481
|
-
} catch (e) {
|
|
482
|
-
return 0;
|
|
483
|
-
}
|
|
288
|
+
final dateA = DateTime.parse(a['date'] as String);
|
|
289
|
+
final dateB = DateTime.parse(b['date'] as String);
|
|
290
|
+
return dateB.compareTo(dateA);
|
|
484
291
|
});
|
|
485
292
|
|
|
486
293
|
return DataTable(
|
|
@@ -496,7 +303,7 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
496
303
|
DataColumn(label: Text('Return')),
|
|
497
304
|
DataColumn(label: Text('% Return')),
|
|
498
305
|
DataColumn(label: Text('Book Cost')),
|
|
499
|
-
DataColumn(label: Text('Actions')),
|
|
306
|
+
DataColumn(label: Text('Actions')),
|
|
500
307
|
],
|
|
501
308
|
rows: sortedTrades.map((trade) {
|
|
502
309
|
final double shares = double.parse(trade['shares'].toString());
|
|
@@ -505,8 +312,8 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
505
312
|
final tradeDate = DateTime.parse(trade['date'] as String);
|
|
506
313
|
final tradeAge = DateTime.now().difference(tradeDate);
|
|
507
314
|
final symbol = trade['asset']?['symbol'] ?? '';
|
|
508
|
-
final livePrice =
|
|
509
|
-
final int tradeId = trade['id'] as int;
|
|
315
|
+
final livePrice = viewModel.livePrices[symbol];
|
|
316
|
+
final int tradeId = trade['id'] as int;
|
|
510
317
|
|
|
511
318
|
double? tradeReturnValue, tradePercentageReturn, tradeBookCost;
|
|
512
319
|
|
|
@@ -514,8 +321,9 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
514
321
|
final currentMarketValue = livePrice * shares;
|
|
515
322
|
tradeReturnValue = currentMarketValue - total;
|
|
516
323
|
tradeBookCost = currentMarketValue;
|
|
517
|
-
if (total > 0)
|
|
324
|
+
if (total > 0) {
|
|
518
325
|
tradePercentageReturn = (tradeReturnValue / total) * 100;
|
|
326
|
+
}
|
|
519
327
|
}
|
|
520
328
|
|
|
521
329
|
final returnColor =
|
|
@@ -532,22 +340,28 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
532
340
|
? Text(_formatCurrency(livePrice))
|
|
533
341
|
: const Text('N/A')),
|
|
534
342
|
DataCell(Text(_formatCurrency(total))),
|
|
535
|
-
DataCell(
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
+
),
|
|
547
361
|
DataCell(
|
|
548
362
|
IconButton(
|
|
549
363
|
icon: const Icon(Icons.delete, color: Colors.red),
|
|
550
|
-
onPressed: () => _confirmAndDeleteTrade(tradeId),
|
|
364
|
+
onPressed: () => _confirmAndDeleteTrade(context, viewModel, tradeId),
|
|
551
365
|
tooltip: 'Delete Trade',
|
|
552
366
|
),
|
|
553
367
|
),
|
|
@@ -556,33 +370,20 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
556
370
|
);
|
|
557
371
|
}
|
|
558
372
|
|
|
559
|
-
Future<void> _confirmAndDeleteTrade(
|
|
373
|
+
Future<void> _confirmAndDeleteTrade(BuildContext context,
|
|
374
|
+
InvestmentAccountViewModel viewModel, int tradeId) async {
|
|
560
375
|
final bool? confirm =
|
|
561
376
|
await AppDialogs.showDeleteConfirmationDialog(context, 'this trade');
|
|
562
377
|
|
|
563
378
|
if (confirm == true) {
|
|
564
|
-
|
|
379
|
+
final success = await viewModel.deleteTrade(tradeId);
|
|
380
|
+
if (ScaffoldMessenger.of(context).mounted) {
|
|
565
381
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
final success =
|
|
572
|
-
await _transactionService.deleteTrade(tradeId, token: _token);
|
|
573
|
-
if (success) {
|
|
574
|
-
ScaffoldMessenger.of(context).showSnackBar(
|
|
575
|
-
const SnackBar(content: Text('Trade deleted successfully.')),
|
|
576
|
-
);
|
|
577
|
-
_fetchData(); // Refresh data after deletion
|
|
578
|
-
} else {
|
|
579
|
-
ScaffoldMessenger.of(context).showSnackBar(
|
|
580
|
-
const SnackBar(content: Text('Failed to delete trade.')),
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
|
-
} catch (e) {
|
|
584
|
-
ScaffoldMessenger.of(context).showSnackBar(
|
|
585
|
-
SnackBar(content: Text('Error deleting trade: $e')),
|
|
382
|
+
SnackBar(
|
|
383
|
+
content: Text(success
|
|
384
|
+
? 'Trade deleted successfully.'
|
|
385
|
+
: 'Failed to delete trade.'),
|
|
386
|
+
),
|
|
586
387
|
);
|
|
587
388
|
}
|
|
588
389
|
}
|