@marcos_feitoza/personal-finance-frontend-feature-investments 1.1.1 → 1.2.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/CHANGELOG.md +20 -0
- package/lib/screens/crypto_account_screen.dart +169 -272
- package/lib/screens/investment_account_screen.dart +203 -460
- package/lib/screens/rrsp_sun_life_screen.dart +121 -216
- package/lib/viewmodels/crypto_account_viewmodel.dart +192 -0
- package/lib/viewmodels/investment_account_viewmodel.dart +272 -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('Account History',
|
|
118
|
+
style: Theme.of(context).textTheme.titleLarge),
|
|
119
|
+
const SizedBox(height: 8),
|
|
120
|
+
_buildAnaliticoFilter(context, viewModel),
|
|
121
|
+
const SizedBox(height: 8),
|
|
122
|
+
_buildHistoryTable(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,35 +195,22 @@ 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 (widget.showDividends) ...[
|
|
396
|
-
const DataColumn(label: Text('Dividends')),
|
|
397
|
-
const DataColumn(label: Text('Total Return')),
|
|
398
|
-
const DataColumn(label: Text('% Total Return')),
|
|
399
|
-
],
|
|
400
198
|
const DataColumn(label: Text('Account %')),
|
|
401
199
|
const DataColumn(label: Text('Portfolio %')),
|
|
402
200
|
],
|
|
403
|
-
rows:
|
|
201
|
+
rows: viewModel.portfolioSummary.map((position) {
|
|
404
202
|
final symbol = position['symbol'] as String;
|
|
405
|
-
final shares = position['shares']
|
|
406
|
-
final avgPrice = position['avg_price']
|
|
407
|
-
final bookCost = position['book_cost']
|
|
408
|
-
final livePrice =
|
|
409
|
-
final marketValue = position['market_value']
|
|
410
|
-
final unrealizedPL = position['unrealized_pl']
|
|
411
|
-
final percentUnrealizedPL =
|
|
412
|
-
|
|
413
|
-
final
|
|
414
|
-
|
|
415
|
-
final
|
|
416
|
-
final accountAllocationPercent =
|
|
417
|
-
position['account_allocation_percent'] as double;
|
|
418
|
-
final portfolioAllocationPercent =
|
|
419
|
-
position['portfolio_allocation_percent'] as double;
|
|
420
|
-
|
|
421
|
-
final plColor = (unrealizedPL ?? 0) >= 0 ? Colors.green : Colors.red;
|
|
422
|
-
final totalReturnColor =
|
|
423
|
-
(totalReturn ?? 0) >= 0 ? Colors.green : Colors.red;
|
|
203
|
+
final shares = double.tryParse(position['shares']?.toString() ?? '0.0') ?? 0.0;
|
|
204
|
+
final avgPrice = double.tryParse(position['avg_price']?.toString() ?? '0.0') ?? 0.0;
|
|
205
|
+
final bookCost = double.tryParse(position['book_cost']?.toString() ?? '0.0') ?? 0.0;
|
|
206
|
+
final livePrice = viewModel.livePrices[symbol];
|
|
207
|
+
final marketValue = double.tryParse(position['market_value']?.toString() ?? '0.0') ?? 0.0;
|
|
208
|
+
final unrealizedPL = double.tryParse(position['unrealized_pl']?.toString() ?? '0.0') ?? 0.0;
|
|
209
|
+
final percentUnrealizedPL = double.tryParse(position['percent_unrealized_pl']?.toString() ?? '0.0') ?? 0.0;
|
|
210
|
+
final accountAllocationPercent = double.tryParse(position['account_allocation_percent']?.toString() ?? '0.0') ?? 0.0;
|
|
211
|
+
final portfolioAllocationPercent = double.tryParse(position['portfolio_allocation_percent']?.toString() ?? '0.0') ?? 0.0;
|
|
212
|
+
|
|
213
|
+
final plColor = unrealizedPL >= 0 ? Colors.green : Colors.red;
|
|
424
214
|
|
|
425
215
|
return DataRow(cells: [
|
|
426
216
|
DataCell(Text(symbol)),
|
|
@@ -431,17 +221,12 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
431
221
|
? Text(_formatCurrency(livePrice))
|
|
432
222
|
: const Text('N/A')),
|
|
433
223
|
DataCell(Text(_formatCurrency(marketValue))),
|
|
434
|
-
DataCell(
|
|
435
|
-
|
|
224
|
+
DataCell(
|
|
225
|
+
Text(_formatCurrency(unrealizedPL),
|
|
226
|
+
style: TextStyle(color: plColor))),
|
|
436
227
|
DataCell(Text('${percentUnrealizedPL.toStringAsFixed(2)}%',
|
|
437
228
|
style: TextStyle(color: plColor))),
|
|
438
|
-
|
|
439
|
-
DataCell(Text(_formatCurrency(totalDividends))),
|
|
440
|
-
DataCell(Text(_formatCurrency(totalReturn),
|
|
441
|
-
style: TextStyle(color: totalReturnColor))),
|
|
442
|
-
DataCell(Text('${percentTotalReturn.toStringAsFixed(2)}%',
|
|
443
|
-
style: TextStyle(color: totalReturnColor))),
|
|
444
|
-
],
|
|
229
|
+
|
|
445
230
|
DataCell(Text('${accountAllocationPercent.toStringAsFixed(2)}%')),
|
|
446
231
|
DataCell(Text('${portfolioAllocationPercent.toStringAsFixed(2)}%')),
|
|
447
232
|
]);
|
|
@@ -450,38 +235,33 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
450
235
|
);
|
|
451
236
|
}
|
|
452
237
|
|
|
453
|
-
Widget
|
|
454
|
-
|
|
238
|
+
Widget _buildHistoryTable(
|
|
239
|
+
BuildContext context, InvestmentAccountViewModel viewModel) {
|
|
240
|
+
if (viewModel.unifiedHistory.isEmpty) {
|
|
455
241
|
return const Center(
|
|
456
|
-
|
|
457
|
-
|
|
242
|
+
child: Padding(
|
|
243
|
+
padding: EdgeInsets.all(16.0),
|
|
244
|
+
child: Text('No history found.'),
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
458
248
|
|
|
459
|
-
final List<Map<String, dynamic>>
|
|
460
|
-
|
|
461
|
-
?
|
|
462
|
-
:
|
|
463
|
-
.where((
|
|
464
|
-
|
|
249
|
+
final List<Map<String, dynamic>> filteredHistory =
|
|
250
|
+
viewModel.selectedSymbolForFilter == null
|
|
251
|
+
? viewModel.unifiedHistory
|
|
252
|
+
: viewModel.unifiedHistory
|
|
253
|
+
.where((item) =>
|
|
254
|
+
item['symbol'] == viewModel.selectedSymbolForFilter)
|
|
465
255
|
.toList();
|
|
466
256
|
|
|
467
|
-
if (
|
|
257
|
+
if (filteredHistory.isEmpty) {
|
|
468
258
|
return const Center(
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
final idA = int.tryParse(a['id']?.toString() ?? '');
|
|
476
|
-
final idB = int.tryParse(b['id']?.toString() ?? '');
|
|
477
|
-
if (idA != null && idB != null) return idB.compareTo(idA);
|
|
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
|
-
}
|
|
484
|
-
});
|
|
259
|
+
child: Padding(
|
|
260
|
+
padding: EdgeInsets.all(16.0),
|
|
261
|
+
child: Text('No history matches the selected symbol.'),
|
|
262
|
+
),
|
|
263
|
+
);
|
|
264
|
+
}
|
|
485
265
|
|
|
486
266
|
return DataTable(
|
|
487
267
|
columns: const [
|
|
@@ -491,64 +271,36 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
491
271
|
DataColumn(label: Text('Type')),
|
|
492
272
|
DataColumn(label: Text('Shares')),
|
|
493
273
|
DataColumn(label: Text('Price')),
|
|
494
|
-
DataColumn(label: Text('Live')),
|
|
495
274
|
DataColumn(label: Text('Total')),
|
|
496
|
-
DataColumn(label: Text('
|
|
497
|
-
DataColumn(label: Text('% Return')),
|
|
498
|
-
DataColumn(label: Text('Book Cost')),
|
|
499
|
-
DataColumn(label: Text('Actions')), // New column for actions
|
|
275
|
+
DataColumn(label: Text('Actions')),
|
|
500
276
|
],
|
|
501
|
-
rows:
|
|
502
|
-
final
|
|
503
|
-
final
|
|
504
|
-
final double total = shares * price;
|
|
505
|
-
final tradeDate = DateTime.parse(trade['date'] as String);
|
|
506
|
-
final tradeAge = DateTime.now().difference(tradeDate);
|
|
507
|
-
final symbol = trade['asset']?['symbol'] ?? '';
|
|
508
|
-
final livePrice = _livePrices[symbol];
|
|
509
|
-
final int tradeId = trade['id'] as int; // Get trade ID
|
|
510
|
-
|
|
511
|
-
double? tradeReturnValue, tradePercentageReturn, tradeBookCost;
|
|
277
|
+
rows: filteredHistory.map((item) {
|
|
278
|
+
final isTrade = item['history_type'] == 'trade';
|
|
279
|
+
final isDividend = item['history_type'] == 'dividend';
|
|
512
280
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
tradePercentageReturn = (tradeReturnValue / total) * 100;
|
|
519
|
-
}
|
|
281
|
+
final tradeDate = DateTime.parse(item['date'] as String);
|
|
282
|
+
final tradeAge = DateTime.now().difference(tradeDate);
|
|
283
|
+
final symbol = item['symbol'] as String? ?? 'N/A';
|
|
284
|
+
final type = item['type'] as String? ?? 'N/A';
|
|
285
|
+
final int id = item['id'] as int;
|
|
520
286
|
|
|
521
|
-
final
|
|
522
|
-
|
|
287
|
+
final double shares = isTrade ? (double.tryParse(item['shares']?.toString() ?? '0.0') ?? 0.0) : 0.0;
|
|
288
|
+
final double price = isTrade ? (double.tryParse(item['price']?.toString() ?? '0.0') ?? 0.0) : 0.0;
|
|
289
|
+
final double total = isTrade ? (shares * price) : (double.tryParse(item['total']?.toString() ?? '0.0') ?? 0.0);
|
|
523
290
|
|
|
524
291
|
return DataRow(cells: [
|
|
525
292
|
DataCell(Text(DateFormat('yyyy-MM-dd').format(tradeDate))),
|
|
526
293
|
DataCell(Text(_formatDuration(tradeAge))),
|
|
527
294
|
DataCell(Text(symbol)),
|
|
528
|
-
DataCell(Text(
|
|
529
|
-
DataCell(Text(shares.
|
|
530
|
-
DataCell(Text(_formatCurrency(price))),
|
|
531
|
-
DataCell(livePrice != null
|
|
532
|
-
? Text(_formatCurrency(livePrice))
|
|
533
|
-
: const Text('N/A')),
|
|
295
|
+
DataCell(Text(type)),
|
|
296
|
+
DataCell(isTrade ? Text(shares.toStringAsFixed(4)) : const Text('-')),
|
|
297
|
+
DataCell(isTrade ? Text(_formatCurrency(price)) : const Text('-')),
|
|
534
298
|
DataCell(Text(_formatCurrency(total))),
|
|
535
|
-
DataCell(tradeReturnValue != null
|
|
536
|
-
? Text(_formatCurrency(tradeReturnValue),
|
|
537
|
-
style: TextStyle(color: returnColor))
|
|
538
|
-
: const Text('N/A')),
|
|
539
|
-
DataCell(tradePercentageReturn != null
|
|
540
|
-
? Text('${tradePercentageReturn.toStringAsFixed(2)}%',
|
|
541
|
-
style: TextStyle(color: returnColor))
|
|
542
|
-
: const Text('N/A')),
|
|
543
|
-
DataCell(tradeBookCost != null
|
|
544
|
-
? Text(_formatCurrency(tradeBookCost),
|
|
545
|
-
style: TextStyle(color: returnColor))
|
|
546
|
-
: const Text('N/A')),
|
|
547
299
|
DataCell(
|
|
548
300
|
IconButton(
|
|
549
301
|
icon: const Icon(Icons.delete, color: Colors.red),
|
|
550
|
-
onPressed: () =>
|
|
551
|
-
tooltip: 'Delete
|
|
302
|
+
onPressed: () => _confirmAndDeleteItem(context, viewModel, id, isTrade),
|
|
303
|
+
tooltip: 'Delete Item',
|
|
552
304
|
),
|
|
553
305
|
),
|
|
554
306
|
]);
|
|
@@ -556,33 +308,24 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
556
308
|
);
|
|
557
309
|
}
|
|
558
310
|
|
|
559
|
-
Future<void>
|
|
311
|
+
Future<void> _confirmAndDeleteItem(BuildContext context,
|
|
312
|
+
InvestmentAccountViewModel viewModel, int itemId, bool isTrade) async {
|
|
313
|
+
final itemType = isTrade ? 'trade' : 'dividend';
|
|
560
314
|
final bool? confirm =
|
|
561
|
-
await AppDialogs.showDeleteConfirmationDialog(context, 'this
|
|
315
|
+
await AppDialogs.showDeleteConfirmationDialog(context, 'this $itemType');
|
|
562
316
|
|
|
563
317
|
if (confirm == true) {
|
|
564
|
-
|
|
318
|
+
final success = isTrade
|
|
319
|
+
? await viewModel.deleteTrade(itemId)
|
|
320
|
+
: await viewModel.deleteDividend(itemId);
|
|
321
|
+
|
|
322
|
+
if (ScaffoldMessenger.of(context).mounted) {
|
|
565
323
|
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')),
|
|
324
|
+
SnackBar(
|
|
325
|
+
content: Text(success
|
|
326
|
+
? 'The $itemType was deleted successfully.'
|
|
327
|
+
: 'Failed to delete the $itemType.'),
|
|
328
|
+
),
|
|
586
329
|
);
|
|
587
330
|
}
|
|
588
331
|
}
|