@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.
@@ -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,294 +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(
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
- .where((s) => s.isNotEmpty)
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: _selectedSymbolForFilter,
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
- if (_portfolioSummary.isEmpty)
174
+ Widget _buildSinteticoTable(
175
+ BuildContext context, InvestmentAccountViewModel viewModel) {
176
+ if (viewModel.portfolioSummary.isEmpty) {
377
177
  return const Center(
378
- child: Padding(
379
- padding: EdgeInsets.all(16.0),
380
- child: Text('No positions held.')));
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 (widget.showDividends) ...[
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: _portfolioSummary.map((position) {
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 = _livePrices[symbol];
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 = position['percent_total_return'] as double;
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 = (unrealizedPL ?? 0) >= 0 ? Colors.green : Colors.red;
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(Text(_formatCurrency(unrealizedPL),
435
- style: TextStyle(color: plColor))),
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 (widget.showDividends) ...[
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
- if (_trades.isEmpty)
257
+ Widget _buildAnaliticoTable(
258
+ BuildContext context, InvestmentAccountViewModel viewModel) {
259
+ if (viewModel.trades.isEmpty) {
455
260
  return const Center(
456
- child: Padding(
457
- 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
+ }
458
267
 
459
268
  final List<Map<String, dynamic>> filteredTrades =
460
- _selectedSymbolForFilter == null
461
- ? _trades
462
- : _trades
269
+ viewModel.selectedSymbolForFilter == null
270
+ ? viewModel.trades
271
+ : viewModel.trades
463
272
  .where((trade) =>
464
- trade['asset']?['symbol'] == _selectedSymbolForFilter)
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
- child: Padding(
470
- padding: EdgeInsets.all(16.0),
471
- 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
+ }
472
285
 
473
286
  final sortedTrades = List<Map<String, dynamic>>.from(filteredTrades)
474
287
  ..sort((a, b) {
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
- }
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')), // New column for 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 = _livePrices[symbol];
509
- final int tradeId = trade['id'] as int; // Get trade ID
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(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')),
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(int tradeId) async {
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
- if (_token == null) {
379
+ final success = await viewModel.deleteTrade(tradeId);
380
+ if (ScaffoldMessenger.of(context).mounted) {
565
381
  ScaffoldMessenger.of(context).showSnackBar(
566
- const SnackBar(content: Text('Authentication token not available.')),
567
- );
568
- return;
569
- }
570
- try {
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
  }