@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 Center(child: CircularProgressIndicator())
60
- : RefreshIndicator(
61
- onRefresh: viewModel.fetchData,
62
- child: SingleChildScrollView(
63
- physics: const AlwaysScrollableScrollPhysics(),
64
- padding: const EdgeInsets.all(16.0),
65
- child: Column(
66
- crossAxisAlignment: CrossAxisAlignment.stretch,
67
- children: [
68
- Row(
69
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
70
- children: [
71
- Text('Portfolio Summary',
72
- style:
73
- Theme.of(context).textTheme.titleLarge),
74
- if (viewModel.isFetchingPrices)
75
- const SizedBox(
76
- height: 20,
77
- width: 20,
78
- child: CircularProgressIndicator(
79
- strokeWidth: 2.0),
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
- _buildSinteticoTable(context, viewModel),
84
- const SizedBox(height: 24),
85
- CryptoTradeForm(
86
- accountName: accountName,
87
- portfolioSummary: viewModel.portfolioSummary,
88
- onTradeCreated: (_) => viewModel.fetchData(),
89
- token: viewModel.token,
90
- ),
91
- const SizedBox(height: 24),
92
- Text('Trade History',
93
- style: Theme.of(context).textTheme.titleLarge),
94
- const SizedBox(height: 8),
95
- _buildAnaliticoTable(context, viewModel),
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 SingleChildScrollView(
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 DataTable(
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 SingleChildScrollView(
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 DataTable(
268
- columns: const [
269
- DataColumn(label: Text('Date')),
270
- DataColumn(label: Text('Trade Age')),
271
- DataColumn(label: Text('Symbol')),
272
- DataColumn(label: Text('Type')),
273
- DataColumn(label: Text('Shares')),
274
- DataColumn(label: Text('Price')),
275
- DataColumn(label: Text('Total')),
276
- DataColumn(label: Text('Actions')),
277
- ],
278
- rows: filteredHistory.map((item) {
279
- final isTrade = item['history_type'] == 'trade';
280
- final isDividend = item['history_type'] == 'dividend';
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
- final tradeDate = DateTime.parse(item['date'] as String);
283
- final tradeAge = DateTime.now().difference(tradeDate);
284
- final symbol = item['symbol'] as String? ?? 'N/A';
285
- final type = item['type'] as String? ?? 'N/A';
286
- final int id = item['id'] as int;
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
- final double shares = isTrade ? (double.tryParse(item['shares']?.toString() ?? '0.0') ?? 0.0) : 0.0;
289
- final double price = isTrade ? (double.tryParse(item['price']?.toString() ?? '0.0') ?? 0.0) : 0.0;
290
- final double total = isTrade ? (shares * price) : (double.tryParse(item['total']?.toString() ?? '0.0') ?? 0.0);
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
- return DataRow(cells: [
293
- DataCell(Text(DateFormat('yyyy-MM-dd').format(tradeDate))),
294
- DataCell(Text(_formatDuration(tradeAge))),
295
- DataCell(Text(symbol)),
296
- DataCell(Text(type)),
297
- DataCell(isTrade ? Text(shares.toStringAsFixed(4)) : const Text('-')),
298
- DataCell(isTrade ? Text(_formatCurrency(price)) : const Text('-')),
299
- DataCell(Text(_formatCurrency(total))),
300
- DataCell(
301
- IconButton(
302
- icon: const Icon(Icons.delete, color: Colors.red),
303
- onPressed: () => _confirmAndDeleteItem(context, viewModel, id, isTrade),
304
- tooltip: 'Delete Item',
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
- }).toList(),
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 DataTable(
104
- columns: const [
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
- rows: [
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 DataTable(
152
- columns: const [
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
- rows: viewModel.contributions.map((c) {
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
- }).toList(),
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
- debugPrint('Error fetching data: $e');
70
+ _errorMessage = 'Failed to load account data: $e';
71
+ debugPrint('[CryptoAccountViewModel] fetchData failed: $e');
67
72
  } finally {
68
73
  _isLoading = false;
69
74
  notifyListeners();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marcos_feitoza/personal-finance-frontend-feature-investments",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },