@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.
@@ -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 StatefulWidget {
11
+ class InvestmentAccountScreen extends StatelessWidget {
13
12
  final String accountName;
14
13
  final bool showDividends;
15
14
 
16
- const InvestmentAccountScreen(
17
- {Key? key, required this.accountName, this.showDividends = true})
18
- : super(key: key);
15
+ const InvestmentAccountScreen({
16
+ Key? key,
17
+ required this.accountName,
18
+ this.showDividends = true,
19
+ }) : super(key: key);
19
20
 
20
21
  @override
21
- _InvestmentAccountScreenState createState() =>
22
- _InvestmentAccountScreenState();
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
- @override
43
- void initState() {
44
- super.initState();
45
- WidgetsBinding.instance.addPostFrameCallback((_) {
46
- final authProvider = Provider.of<AuthProvider>(context, listen: false);
47
- setState(() {
48
- _token = authProvider.token;
49
- });
50
- _fetchData();
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
- Future<void> _fetchData() async {
75
- if (_token == null) return;
76
- setState(() => _isLoading = true);
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
- .where((s) => s.isNotEmpty)
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: _selectedSymbolForFilter,
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
- if (_portfolioSummary.isEmpty)
174
+ Widget _buildSinteticoTable(
175
+ BuildContext context, InvestmentAccountViewModel viewModel) {
176
+ if (viewModel.portfolioSummary.isEmpty) {
370
177
  return const Center(
371
- child: Padding(
372
- padding: EdgeInsets.all(16.0),
373
- child: Text('No positions held.')));
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 (widget.showDividends) ...[
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: _portfolioSummary.map((position) {
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 = _livePrices[symbol];
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 = position['percent_unrealized_pl'] as double;
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 = position['percent_total_return'] as double;
408
- final accountAllocationPercent = position['account_allocation_percent'] as double;
409
- final portfolioAllocationPercent = position['portfolio_allocation_percent'] as double;
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 plColor =
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(Text(_formatCurrency(unrealizedPL), style: TextStyle(color: plColor))),
427
- DataCell(Text('${percentUnrealizedPL.toStringAsFixed(2)}%', style: TextStyle(color: plColor))),
428
- if (widget.showDividends) ...[
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), style: TextStyle(color: totalReturnColor))),
431
- DataCell(Text('${percentTotalReturn.toStringAsFixed(2)}%', style: TextStyle(color: totalReturnColor))),
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
- if (_trades.isEmpty)
257
+ Widget _buildAnaliticoTable(
258
+ BuildContext context, InvestmentAccountViewModel viewModel) {
259
+ if (viewModel.trades.isEmpty) {
443
260
  return const Center(
444
- child: Padding(
445
- padding: EdgeInsets.all(16.0), child: Text('No trades found.')));
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
- _selectedSymbolForFilter == null
449
- ? _trades
450
- : _trades
269
+ viewModel.selectedSymbolForFilter == null
270
+ ? viewModel.trades
271
+ : viewModel.trades
451
272
  .where((trade) =>
452
- trade['asset']?['symbol'] == _selectedSymbolForFilter)
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
- child: Padding(
458
- padding: EdgeInsets.all(16.0),
459
- child: Text('No trades match the selected symbol.')));
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 idA = int.tryParse(a['id']?.toString() ?? '');
464
- final idB = int.tryParse(b['id']?.toString() ?? '');
465
- if (idA != null && idB != null) return idB.compareTo(idA);
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')), // New column for 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 = _livePrices[symbol];
497
- final int tradeId = trade['id'] as int; // Get trade ID
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(tradeReturnValue != null
524
- ? Text(_formatCurrency(tradeReturnValue),
525
- style: TextStyle(color: returnColor))
526
- : const Text('N/A')),
527
- DataCell(tradePercentageReturn != null
528
- ? Text('${tradePercentageReturn.toStringAsFixed(2)}%',
529
- style: TextStyle(color: returnColor))
530
- : const Text('N/A')),
531
- DataCell(tradeBookCost != null
532
- ? Text(_formatCurrency(tradeBookCost),
533
- style: TextStyle(color: returnColor))
534
- : const Text('N/A')),
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(int tradeId) async {
548
- final bool? confirm = await AppDialogs.showDeleteConfirmationDialog(context, 'this trade');
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
- if (_token == null) {
379
+ final success = await viewModel.deleteTrade(tradeId);
380
+ if (ScaffoldMessenger.of(context).mounted) {
552
381
  ScaffoldMessenger.of(context).showSnackBar(
553
- const SnackBar(content: Text('Authentication token not available.')),
554
- );
555
- return;
556
- }
557
- try {
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
  }