@marcos_feitoza/personal-finance-frontend-feature-investments 1.2.1 → 1.2.3
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
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [1.2.3](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.2.2...v1.2.3) (2026-01-30)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* Padronizar mensagens de erro e estados de loading ([3562e80](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/3562e80f02f1829fbb8702637c97abd9e879ca1a))
|
|
7
|
+
|
|
8
|
+
## [1.2.2](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.2.1...v1.2.2) (2026-01-30)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* Web responsivo (mobile/tablet/resize) ([f3aa7b2](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/f3aa7b2edd456ca2fe1d32ea3a7f8798dc053fe9))
|
|
14
|
+
|
|
1
15
|
## [1.2.1](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.2.0...v1.2.1) (2026-01-29)
|
|
2
16
|
|
|
3
17
|
|
|
@@ -3,7 +3,9 @@ import 'package:intl/intl.dart';
|
|
|
3
3
|
import 'package:provider/provider.dart';
|
|
4
4
|
import 'package:personal_finance_frontend_core_services/providers/auth_provider.dart';
|
|
5
5
|
import 'package:personal_finance_frontend_core_ui/utils/app_dialogs.dart';
|
|
6
|
+
import 'package:personal_finance_frontend_core_ui/utils/app_responsive.dart';
|
|
6
7
|
import 'package:personal_finance_frontend_core_ui/utils/app_snackbars.dart';
|
|
8
|
+
import 'package:personal_finance_frontend_core_ui/widgets/app_async_state_view.dart';
|
|
7
9
|
import 'package:personal_finance_frontend_core_ui/widgets/crypto_trade_form.dart';
|
|
8
10
|
import '../viewmodels/crypto_account_viewmodel.dart';
|
|
9
11
|
|
|
@@ -55,45 +57,60 @@ class CryptoAccountScreen extends StatelessWidget {
|
|
|
55
57
|
),
|
|
56
58
|
],
|
|
57
59
|
),
|
|
60
|
+
// NOTE: Don't treat "no trades yet" as a full-screen empty state,
|
|
61
|
+
// otherwise the trade form disappears for new users.
|
|
62
|
+
// Empty states are handled inside the sections (tables).
|
|
58
63
|
body: viewModel.isLoading
|
|
59
|
-
? const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
64
|
+
? const AppAsyncStateView(
|
|
65
|
+
isLoading: true,
|
|
66
|
+
child: SizedBox.shrink(),
|
|
67
|
+
)
|
|
68
|
+
: AppAsyncStateView(
|
|
69
|
+
isLoading: false,
|
|
70
|
+
errorMessage: viewModel.errorMessage,
|
|
71
|
+
onRetry: viewModel.fetchData,
|
|
72
|
+
child: RefreshIndicator(
|
|
73
|
+
onRefresh: viewModel.fetchData,
|
|
74
|
+
child: SingleChildScrollView(
|
|
75
|
+
physics: const AlwaysScrollableScrollPhysics(),
|
|
76
|
+
padding: const EdgeInsets.all(16.0),
|
|
77
|
+
child: Column(
|
|
78
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
79
|
+
children: [
|
|
80
|
+
Row(
|
|
81
|
+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
82
|
+
children: [
|
|
83
|
+
Text(
|
|
84
|
+
'Portfolio Summary',
|
|
85
|
+
style: Theme.of(context).textTheme.titleLarge,
|
|
80
86
|
),
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
87
|
+
if (viewModel.isFetchingPrices)
|
|
88
|
+
const SizedBox(
|
|
89
|
+
height: 20,
|
|
90
|
+
width: 20,
|
|
91
|
+
child: CircularProgressIndicator(
|
|
92
|
+
strokeWidth: 2.0,
|
|
93
|
+
),
|
|
94
|
+
),
|
|
95
|
+
],
|
|
96
|
+
),
|
|
97
|
+
_buildSinteticoTable(context, viewModel),
|
|
98
|
+
const SizedBox(height: 24),
|
|
99
|
+
CryptoTradeForm(
|
|
100
|
+
accountName: accountName,
|
|
101
|
+
portfolioSummary: viewModel.portfolioSummary,
|
|
102
|
+
onTradeCreated: (_) => viewModel.fetchData(),
|
|
103
|
+
token: viewModel.token,
|
|
104
|
+
),
|
|
105
|
+
const SizedBox(height: 24),
|
|
106
|
+
Text(
|
|
107
|
+
'Trade History',
|
|
108
|
+
style: Theme.of(context).textTheme.titleLarge,
|
|
109
|
+
),
|
|
110
|
+
const SizedBox(height: 8),
|
|
111
|
+
_buildAnaliticoTable(context, viewModel),
|
|
112
|
+
],
|
|
113
|
+
),
|
|
97
114
|
),
|
|
98
115
|
),
|
|
99
116
|
),
|
|
@@ -119,8 +136,7 @@ class CryptoAccountScreen extends StatelessWidget {
|
|
|
119
136
|
);
|
|
120
137
|
}
|
|
121
138
|
|
|
122
|
-
return
|
|
123
|
-
scrollDirection: Axis.horizontal,
|
|
139
|
+
return ResponsiveHorizontalScroll(
|
|
124
140
|
child: DataTable(
|
|
125
141
|
columnSpacing: 24.0,
|
|
126
142
|
columns: const [
|
|
@@ -205,7 +221,8 @@ class CryptoAccountScreen extends StatelessWidget {
|
|
|
205
221
|
}
|
|
206
222
|
});
|
|
207
223
|
|
|
208
|
-
return
|
|
224
|
+
return ResponsiveHorizontalScroll(
|
|
225
|
+
child: DataTable(
|
|
209
226
|
columns: const [
|
|
210
227
|
DataColumn(label: Text('Date')),
|
|
211
228
|
DataColumn(label: Text('Symbol')),
|
|
@@ -239,6 +256,7 @@ class CryptoAccountScreen extends StatelessWidget {
|
|
|
239
256
|
),
|
|
240
257
|
]);
|
|
241
258
|
}).toList(),
|
|
259
|
+
),
|
|
242
260
|
);
|
|
243
261
|
}
|
|
244
262
|
|
|
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
|
|
4
4
|
import 'package:personal_finance_frontend_core_services/providers/auth_provider.dart';
|
|
5
5
|
import 'package:personal_finance_frontend_core_ui/utils/app_dialogs.dart';
|
|
6
6
|
import 'package:personal_finance_frontend_core_ui/utils/app_snackbars.dart';
|
|
7
|
+
import 'package:personal_finance_frontend_core_ui/utils/app_responsive.dart';
|
|
7
8
|
import 'package:personal_finance_frontend_core_ui/widgets/trade_form.dart';
|
|
8
9
|
import 'package:personal_finance_frontend_core_ui/widgets/dividend_log_form.dart';
|
|
9
10
|
import 'package:personal_finance_frontend_core_ui/widgets/app_widgets.dart';
|
|
@@ -183,8 +184,7 @@ class InvestmentAccountScreen extends StatelessWidget {
|
|
|
183
184
|
);
|
|
184
185
|
}
|
|
185
186
|
|
|
186
|
-
return
|
|
187
|
-
scrollDirection: Axis.horizontal,
|
|
187
|
+
return ResponsiveHorizontalScroll(
|
|
188
188
|
child: DataTable(
|
|
189
189
|
columnSpacing: 24.0,
|
|
190
190
|
columns: [
|
|
@@ -264,48 +264,57 @@ class InvestmentAccountScreen extends StatelessWidget {
|
|
|
264
264
|
);
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
-
return
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
267
|
+
return ResponsiveHorizontalScroll(
|
|
268
|
+
child: DataTable(
|
|
269
|
+
columns: const [
|
|
270
|
+
DataColumn(label: Text('Date')),
|
|
271
|
+
DataColumn(label: Text('Trade Age')),
|
|
272
|
+
DataColumn(label: Text('Symbol')),
|
|
273
|
+
DataColumn(label: Text('Type')),
|
|
274
|
+
DataColumn(label: Text('Shares')),
|
|
275
|
+
DataColumn(label: Text('Price')),
|
|
276
|
+
DataColumn(label: Text('Total')),
|
|
277
|
+
DataColumn(label: Text('Actions')),
|
|
278
|
+
],
|
|
279
|
+
rows: filteredHistory.map((item) {
|
|
280
|
+
final isTrade = item['history_type'] == 'trade';
|
|
281
|
+
// final isDividend = item['history_type'] == 'dividend'; // Reserved for future use.
|
|
281
282
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
283
|
+
final tradeDate = DateTime.parse(item['date'] as String);
|
|
284
|
+
final tradeAge = DateTime.now().difference(tradeDate);
|
|
285
|
+
final symbol = item['symbol'] as String? ?? 'N/A';
|
|
286
|
+
final type = item['type'] as String? ?? 'N/A';
|
|
287
|
+
final int id = item['id'] as int;
|
|
287
288
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
289
|
+
final double shares = isTrade
|
|
290
|
+
? (double.tryParse(item['shares']?.toString() ?? '0.0') ?? 0.0)
|
|
291
|
+
: 0.0;
|
|
292
|
+
final double price = isTrade
|
|
293
|
+
? (double.tryParse(item['price']?.toString() ?? '0.0') ?? 0.0)
|
|
294
|
+
: 0.0;
|
|
295
|
+
final double total = isTrade
|
|
296
|
+
? (shares * price)
|
|
297
|
+
: (double.tryParse(item['total']?.toString() ?? '0.0') ?? 0.0);
|
|
291
298
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
299
|
+
return DataRow(cells: [
|
|
300
|
+
DataCell(Text(DateFormat('yyyy-MM-dd').format(tradeDate))),
|
|
301
|
+
DataCell(Text(_formatDuration(tradeAge))),
|
|
302
|
+
DataCell(Text(symbol)),
|
|
303
|
+
DataCell(Text(type)),
|
|
304
|
+
DataCell(isTrade ? Text(shares.toStringAsFixed(4)) : const Text('-')),
|
|
305
|
+
DataCell(isTrade ? Text(_formatCurrency(price)) : const Text('-')),
|
|
306
|
+
DataCell(Text(_formatCurrency(total))),
|
|
307
|
+
DataCell(
|
|
308
|
+
IconButton(
|
|
309
|
+
icon: const Icon(Icons.delete, color: Colors.red),
|
|
310
|
+
onPressed: () =>
|
|
311
|
+
_confirmAndDeleteItem(context, viewModel, id, isTrade),
|
|
312
|
+
tooltip: 'Delete Item',
|
|
313
|
+
),
|
|
305
314
|
),
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
|
|
315
|
+
]);
|
|
316
|
+
}).toList(),
|
|
317
|
+
),
|
|
309
318
|
);
|
|
310
319
|
}
|
|
311
320
|
|
|
@@ -3,6 +3,7 @@ import 'package:intl/intl.dart';
|
|
|
3
3
|
import 'package:provider/provider.dart';
|
|
4
4
|
import 'package:personal_finance_frontend_core_services/providers/auth_provider.dart';
|
|
5
5
|
import 'package:personal_finance_frontend_core_ui/utils/app_dialogs.dart';
|
|
6
|
+
import 'package:personal_finance_frontend_core_ui/utils/app_responsive.dart';
|
|
6
7
|
import 'package:personal_finance_frontend_core_ui/utils/app_snackbars.dart';
|
|
7
8
|
import 'package:personal_finance_frontend_core_ui/widgets/rrsp_contribution_form.dart';
|
|
8
9
|
import '../viewmodels/rrsp_sun_life_viewmodel.dart';
|
|
@@ -100,8 +101,9 @@ class RrspSunLifeScreen extends StatelessWidget {
|
|
|
100
101
|
final double unrealizedPL = double.tryParse(summary['unrealized_pl']?.toString() ?? '0.0') ?? 0.0;
|
|
101
102
|
final plColor = (unrealizedPL >= 0) ? Colors.green : Colors.red;
|
|
102
103
|
|
|
103
|
-
return
|
|
104
|
-
|
|
104
|
+
return ResponsiveHorizontalScroll(
|
|
105
|
+
child: DataTable(
|
|
106
|
+
columns: const [
|
|
105
107
|
DataColumn(label: Text('User Contr.')),
|
|
106
108
|
DataColumn(label: Text('Company Contr.')),
|
|
107
109
|
DataColumn(label: Text('Total Contributed')),
|
|
@@ -110,7 +112,7 @@ class RrspSunLifeScreen extends StatelessWidget {
|
|
|
110
112
|
DataColumn(label: Text('Market Value')),
|
|
111
113
|
DataColumn(label: Text('Portfolio %')),
|
|
112
114
|
],
|
|
113
|
-
|
|
115
|
+
rows: [
|
|
114
116
|
DataRow(
|
|
115
117
|
cells: [
|
|
116
118
|
DataCell(
|
|
@@ -133,7 +135,8 @@ class RrspSunLifeScreen extends StatelessWidget {
|
|
|
133
135
|
),
|
|
134
136
|
],
|
|
135
137
|
),
|
|
136
|
-
|
|
138
|
+
],
|
|
139
|
+
),
|
|
137
140
|
);
|
|
138
141
|
}
|
|
139
142
|
|
|
@@ -148,8 +151,9 @@ class RrspSunLifeScreen extends StatelessWidget {
|
|
|
148
151
|
);
|
|
149
152
|
}
|
|
150
153
|
|
|
151
|
-
return
|
|
152
|
-
|
|
154
|
+
return ResponsiveHorizontalScroll(
|
|
155
|
+
child: DataTable(
|
|
156
|
+
columns: const [
|
|
153
157
|
DataColumn(label: Text('Date')),
|
|
154
158
|
DataColumn(label: Text('User Contr.')),
|
|
155
159
|
DataColumn(label: Text('Company Contr.')),
|
|
@@ -160,7 +164,7 @@ class RrspSunLifeScreen extends StatelessWidget {
|
|
|
160
164
|
DataColumn(label: Text('Portfolio %')),
|
|
161
165
|
DataColumn(label: Text('Actions')),
|
|
162
166
|
],
|
|
163
|
-
|
|
167
|
+
rows: viewModel.contributions.map((c) {
|
|
164
168
|
final contributionId = c['id'] as int;
|
|
165
169
|
final rrspAmount = double.tryParse(c['rrsp_amount']?.toString() ?? '0.0') ?? 0.0;
|
|
166
170
|
final dpspAmount = double.tryParse(c['dpsp_amount']?.toString() ?? '0.0') ?? 0.0;
|
|
@@ -207,7 +211,8 @@ class RrspSunLifeScreen extends StatelessWidget {
|
|
|
207
211
|
),
|
|
208
212
|
],
|
|
209
213
|
);
|
|
210
|
-
|
|
214
|
+
}).toList(),
|
|
215
|
+
),
|
|
211
216
|
);
|
|
212
217
|
}
|
|
213
218
|
|
|
@@ -18,6 +18,8 @@ class CryptoAccountViewModel extends ChangeNotifier {
|
|
|
18
18
|
bool _isLoading = true;
|
|
19
19
|
bool _isFetchingPrices = false;
|
|
20
20
|
|
|
21
|
+
String? _errorMessage;
|
|
22
|
+
|
|
21
23
|
// Getters
|
|
22
24
|
List<Map<String, dynamic>> get trades => _trades;
|
|
23
25
|
List<Map<String, dynamic>> get assets => _assets;
|
|
@@ -27,6 +29,7 @@ class CryptoAccountViewModel extends ChangeNotifier {
|
|
|
27
29
|
double get accountTotalValue => _accountTotalValue;
|
|
28
30
|
bool get isLoading => _isLoading;
|
|
29
31
|
bool get isFetchingPrices => _isFetchingPrices;
|
|
32
|
+
String? get errorMessage => _errorMessage;
|
|
30
33
|
String? get token => _token;
|
|
31
34
|
|
|
32
35
|
CryptoAccountViewModel({required this.accountName, required String? token}) {
|
|
@@ -42,6 +45,7 @@ class CryptoAccountViewModel extends ChangeNotifier {
|
|
|
42
45
|
Future<void> fetchData() async {
|
|
43
46
|
if (_token == null) return;
|
|
44
47
|
_isLoading = true;
|
|
48
|
+
_errorMessage = null;
|
|
45
49
|
notifyListeners();
|
|
46
50
|
|
|
47
51
|
try {
|
|
@@ -63,7 +67,8 @@ class CryptoAccountViewModel extends ChangeNotifier {
|
|
|
63
67
|
await _fetchLivePrices();
|
|
64
68
|
|
|
65
69
|
} catch (e) {
|
|
66
|
-
|
|
70
|
+
_errorMessage = 'Failed to load account data: $e';
|
|
71
|
+
debugPrint('[CryptoAccountViewModel] fetchData failed: $e');
|
|
67
72
|
} finally {
|
|
68
73
|
_isLoading = false;
|
|
69
74
|
notifyListeners();
|