@marcos_feitoza/personal-finance-frontend-feature-dashboard 1.2.0 → 1.2.1
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 +8 -0
- package/lib/models/dashboard_widget.dart +19 -0
- package/lib/screens/home_screen.dart +276 -69
- package/lib/utils/dashboard_layout_storage.dart +21 -0
- package/package.json +1 -1
- package/pubspec.yaml +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## [1.2.1](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/compare/v1.2.0...v1.2.1) (2026-01-30)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* Reorder no mobile ([1342ce5](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/commit/1342ce51d9f0c1b4ea2fc684dfe756d1835b252e))
|
|
7
|
+
* Web responsivo (mobile/tablet/resize) ([9ad887a](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/commit/9ad887ae839e6bb70edfe2f50877f00dc36d4851))
|
|
8
|
+
|
|
1
9
|
# [1.2.0](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/compare/v1.1.2...v1.2.0) (2026-01-29)
|
|
2
10
|
|
|
3
11
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import 'package:flutter/widgets.dart';
|
|
2
|
+
|
|
3
|
+
/// A lightweight definition of a dashboard card/widget.
|
|
4
|
+
///
|
|
5
|
+
/// The goal is to make the dashboard layout data-driven so we can:
|
|
6
|
+
/// - reorder widgets (mobile MVP)
|
|
7
|
+
/// - persist order locally
|
|
8
|
+
/// - later allow hide/show and desktop grid reorder
|
|
9
|
+
class DashboardWidgetDefinition {
|
|
10
|
+
final String id;
|
|
11
|
+
final String title;
|
|
12
|
+
final WidgetBuilder builder;
|
|
13
|
+
|
|
14
|
+
const DashboardWidgetDefinition({
|
|
15
|
+
required this.id,
|
|
16
|
+
required this.title,
|
|
17
|
+
required this.builder,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -17,6 +17,7 @@ import 'package:intl/intl.dart';
|
|
|
17
17
|
import 'package:personal_finance_frontend_feature_management/screens/reconciliation_screen.dart';
|
|
18
18
|
import 'package:personal_finance_frontend_core_ui/widgets/app_widgets.dart';
|
|
19
19
|
import 'package:personal_finance_frontend_core_ui/utils/theme_notifier.dart';
|
|
20
|
+
import 'package:personal_finance_frontend_core_ui/utils/app_responsive.dart';
|
|
20
21
|
import 'package:personal_finance_frontend_core_services/providers/auth_provider.dart';
|
|
21
22
|
import 'package:personal_finance_frontend_core_ui/widgets/user_profile_avatar.dart';
|
|
22
23
|
import 'package:personal_finance_frontend_feature_profile/screens/profile_screen.dart';
|
|
@@ -24,6 +25,9 @@ import '../viewmodels/dashboard_viewmodel.dart';
|
|
|
24
25
|
import 'package:personal_finance_frontend_core_ui/utils/currency_input_formatter.dart';
|
|
25
26
|
import 'package:personal_finance_frontend_core_ui/theme/app_colors.dart';
|
|
26
27
|
|
|
28
|
+
import '../models/dashboard_widget.dart';
|
|
29
|
+
import '../utils/dashboard_layout_storage.dart';
|
|
30
|
+
|
|
27
31
|
final String currencySymbol = r'$';
|
|
28
32
|
|
|
29
33
|
String _formatCurrency(double value) {
|
|
@@ -35,9 +39,189 @@ String _formatCurrency(double value) {
|
|
|
35
39
|
return format.format(value);
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
class HomeScreen extends
|
|
42
|
+
class HomeScreen extends StatefulWidget {
|
|
39
43
|
const HomeScreen({Key? key}) : super(key: key);
|
|
40
44
|
|
|
45
|
+
@override
|
|
46
|
+
State<HomeScreen> createState() => _HomeScreenState();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class _HomeScreenState extends State<HomeScreen> {
|
|
50
|
+
final DashboardLayoutStorage _layoutStorage = DashboardLayoutStorage();
|
|
51
|
+
List<String>? _mobileOrder;
|
|
52
|
+
bool _mobileOrderLoaded = false;
|
|
53
|
+
|
|
54
|
+
@override
|
|
55
|
+
void initState() {
|
|
56
|
+
super.initState();
|
|
57
|
+
_loadMobileOrder();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
Future<void> _loadMobileOrder() async {
|
|
61
|
+
final saved = await _layoutStorage.loadOrder();
|
|
62
|
+
if (!mounted) return;
|
|
63
|
+
setState(() {
|
|
64
|
+
_mobileOrder = saved;
|
|
65
|
+
_mobileOrderLoaded = true;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
List<DashboardWidgetDefinition> _defaultMobileWidgets(
|
|
70
|
+
BuildContext context,
|
|
71
|
+
DashboardViewModel viewModel,
|
|
72
|
+
String? token,
|
|
73
|
+
) {
|
|
74
|
+
return [
|
|
75
|
+
DashboardWidgetDefinition(
|
|
76
|
+
id: 'cash_balance',
|
|
77
|
+
title: 'Cash Balance',
|
|
78
|
+
builder: (context) => _buildCashBalanceCard(context, viewModel),
|
|
79
|
+
),
|
|
80
|
+
DashboardWidgetDefinition(
|
|
81
|
+
id: 'rbc_balance',
|
|
82
|
+
title: 'RBC Balance',
|
|
83
|
+
builder: (context) => _buildCreditCardBalanceCard(
|
|
84
|
+
context,
|
|
85
|
+
viewModel,
|
|
86
|
+
'RBC Balance',
|
|
87
|
+
viewModel.rbcCardController,
|
|
88
|
+
viewModel.rbcUnpaidExpenses,
|
|
89
|
+
),
|
|
90
|
+
),
|
|
91
|
+
DashboardWidgetDefinition(
|
|
92
|
+
id: 'bmo_balance',
|
|
93
|
+
title: 'BMO Balance',
|
|
94
|
+
builder: (context) => _buildCreditCardBalanceCard(
|
|
95
|
+
context,
|
|
96
|
+
viewModel,
|
|
97
|
+
'BMO Balance',
|
|
98
|
+
viewModel.bmoCardController,
|
|
99
|
+
viewModel.bmoUnpaidExpenses,
|
|
100
|
+
),
|
|
101
|
+
),
|
|
102
|
+
DashboardWidgetDefinition(
|
|
103
|
+
id: 'cash_tx',
|
|
104
|
+
title: 'Cash Transactions',
|
|
105
|
+
builder: (context) => _buildRecentTransactionsList(
|
|
106
|
+
'Cash Transactions',
|
|
107
|
+
viewModel.cashTransactions,
|
|
108
|
+
),
|
|
109
|
+
),
|
|
110
|
+
DashboardWidgetDefinition(
|
|
111
|
+
id: 'rbc_tx',
|
|
112
|
+
title: 'RBC Transactions',
|
|
113
|
+
builder: (context) => _buildRecentTransactionsList(
|
|
114
|
+
'RBC Transactions',
|
|
115
|
+
viewModel.rbcTransactions,
|
|
116
|
+
),
|
|
117
|
+
),
|
|
118
|
+
DashboardWidgetDefinition(
|
|
119
|
+
id: 'bmo_tx',
|
|
120
|
+
title: 'BMO Transactions',
|
|
121
|
+
builder: (context) => _buildRecentTransactionsList(
|
|
122
|
+
'BMO Transactions',
|
|
123
|
+
viewModel.bmoTransactions,
|
|
124
|
+
),
|
|
125
|
+
),
|
|
126
|
+
DashboardWidgetDefinition(
|
|
127
|
+
id: 'expense_form',
|
|
128
|
+
title: 'New Expense',
|
|
129
|
+
builder: (context) => ExpenseForm(
|
|
130
|
+
formKey: viewModel.expenseFormKey,
|
|
131
|
+
onTransactionSuccess: viewModel.fetchData,
|
|
132
|
+
token: token,
|
|
133
|
+
),
|
|
134
|
+
),
|
|
135
|
+
DashboardWidgetDefinition(
|
|
136
|
+
id: 'investment_form',
|
|
137
|
+
title: 'Move Money / Investment',
|
|
138
|
+
builder: (context) => InvestmentForm(
|
|
139
|
+
formKey: viewModel.investmentFormKey,
|
|
140
|
+
accountBalances: viewModel.accountBalances,
|
|
141
|
+
cashBalance: viewModel.appCalculatedCash,
|
|
142
|
+
onTransactionSuccess: viewModel.fetchData,
|
|
143
|
+
token: token,
|
|
144
|
+
),
|
|
145
|
+
),
|
|
146
|
+
DashboardWidgetDefinition(
|
|
147
|
+
id: 'salary_form',
|
|
148
|
+
title: 'New Salary',
|
|
149
|
+
builder: (context) => SalaryForm(
|
|
150
|
+
formKey: viewModel.salaryFormKey,
|
|
151
|
+
onTransactionSuccess: viewModel.fetchData,
|
|
152
|
+
token: token,
|
|
153
|
+
),
|
|
154
|
+
),
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
List<DashboardWidgetDefinition> _applyOrder(
|
|
159
|
+
List<DashboardWidgetDefinition> defaults,
|
|
160
|
+
List<String>? order,
|
|
161
|
+
) {
|
|
162
|
+
if (order == null || order.isEmpty) return defaults;
|
|
163
|
+
|
|
164
|
+
final byId = {for (final w in defaults) w.id: w};
|
|
165
|
+
final ordered = <DashboardWidgetDefinition>[];
|
|
166
|
+
|
|
167
|
+
// Keep only known ids
|
|
168
|
+
for (final id in order) {
|
|
169
|
+
final w = byId[id];
|
|
170
|
+
if (w != null) ordered.add(w);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Append new widgets that weren't in the stored order
|
|
174
|
+
for (final w in defaults) {
|
|
175
|
+
if (!ordered.any((x) => x.id == w.id)) {
|
|
176
|
+
ordered.add(w);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return ordered;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
Widget _buildReorderableMobileDashboard(
|
|
184
|
+
BuildContext context,
|
|
185
|
+
DashboardViewModel viewModel,
|
|
186
|
+
String? token,
|
|
187
|
+
) {
|
|
188
|
+
if (!_mobileOrderLoaded) {
|
|
189
|
+
return const Center(child: CircularProgressIndicator());
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
final defaults = _defaultMobileWidgets(context, viewModel, token);
|
|
193
|
+
final widgets = _applyOrder(defaults, _mobileOrder);
|
|
194
|
+
|
|
195
|
+
// ReorderableListView must be scrollable itself.
|
|
196
|
+
// We are already inside a SingleChildScrollView in the Scaffold body.
|
|
197
|
+
// So we disable its scroll and let the parent handle scroll.
|
|
198
|
+
return ReorderableListView(
|
|
199
|
+
shrinkWrap: true,
|
|
200
|
+
physics: const NeverScrollableScrollPhysics(),
|
|
201
|
+
onReorder: (oldIndex, newIndex) async {
|
|
202
|
+
final list = List<DashboardWidgetDefinition>.from(widgets);
|
|
203
|
+
if (newIndex > oldIndex) newIndex -= 1;
|
|
204
|
+
final item = list.removeAt(oldIndex);
|
|
205
|
+
list.insert(newIndex, item);
|
|
206
|
+
|
|
207
|
+
final ids = list.map((w) => w.id).toList();
|
|
208
|
+
setState(() {
|
|
209
|
+
_mobileOrder = ids;
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
await _layoutStorage.saveOrder(ids);
|
|
213
|
+
},
|
|
214
|
+
children: [
|
|
215
|
+
for (final w in widgets)
|
|
216
|
+
Padding(
|
|
217
|
+
key: ValueKey(w.id),
|
|
218
|
+
padding: const EdgeInsets.only(bottom: 16.0),
|
|
219
|
+
child: w.builder(context),
|
|
220
|
+
),
|
|
221
|
+
],
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
41
225
|
@override
|
|
42
226
|
Widget build(BuildContext context) {
|
|
43
227
|
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
|
@@ -101,82 +285,105 @@ class HomeScreen extends StatelessWidget {
|
|
|
101
285
|
? const Center(child: CircularProgressIndicator())
|
|
102
286
|
: SingleChildScrollView(
|
|
103
287
|
padding: const EdgeInsets.all(16.0),
|
|
104
|
-
child:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
288
|
+
child: ResponsiveBuilder(
|
|
289
|
+
// Mobile: stack sections vertically (single column)
|
|
290
|
+
mobile: (context) => _buildReorderableMobileDashboard(
|
|
291
|
+
context,
|
|
292
|
+
viewModel,
|
|
293
|
+
token,
|
|
294
|
+
),
|
|
295
|
+
|
|
296
|
+
// Desktop (and tablet fallback): keep the wide dashboard layout.
|
|
297
|
+
desktop: (context) => Column(
|
|
298
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
299
|
+
children: [
|
|
300
|
+
Row(
|
|
301
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
302
|
+
children: [
|
|
303
|
+
Expanded(
|
|
304
|
+
child: _buildCashBalanceCard(context, viewModel),
|
|
305
|
+
),
|
|
306
|
+
const SizedBox(width: 16),
|
|
307
|
+
Expanded(
|
|
115
308
|
child: _buildCreditCardBalanceCard(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
309
|
+
context,
|
|
310
|
+
viewModel,
|
|
311
|
+
'RBC Balance',
|
|
312
|
+
viewModel.rbcCardController,
|
|
313
|
+
viewModel.rbcUnpaidExpenses,
|
|
314
|
+
),
|
|
315
|
+
),
|
|
316
|
+
const SizedBox(width: 16),
|
|
317
|
+
Expanded(
|
|
123
318
|
child: _buildCreditCardBalanceCard(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
319
|
+
context,
|
|
320
|
+
viewModel,
|
|
321
|
+
'BMO Balance',
|
|
322
|
+
viewModel.bmoCardController,
|
|
323
|
+
viewModel.bmoUnpaidExpenses,
|
|
324
|
+
),
|
|
325
|
+
),
|
|
326
|
+
],
|
|
327
|
+
),
|
|
328
|
+
const SizedBox(height: 16),
|
|
329
|
+
Row(
|
|
330
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
331
|
+
children: [
|
|
332
|
+
Expanded(
|
|
136
333
|
child: _buildRecentTransactionsList(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
334
|
+
'Cash Transactions',
|
|
335
|
+
viewModel.cashTransactions,
|
|
336
|
+
),
|
|
337
|
+
),
|
|
338
|
+
const SizedBox(width: 16),
|
|
339
|
+
Expanded(
|
|
141
340
|
child: _buildRecentTransactionsList(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
341
|
+
'RBC Transactions',
|
|
342
|
+
viewModel.rbcTransactions,
|
|
343
|
+
),
|
|
344
|
+
),
|
|
345
|
+
const SizedBox(width: 16),
|
|
346
|
+
Expanded(
|
|
146
347
|
child: _buildRecentTransactionsList(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
348
|
+
'BMO Transactions',
|
|
349
|
+
viewModel.bmoTransactions,
|
|
350
|
+
),
|
|
351
|
+
),
|
|
352
|
+
const SizedBox(width: 16),
|
|
353
|
+
Expanded(
|
|
151
354
|
child: ExpenseForm(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
355
|
+
formKey: viewModel.expenseFormKey,
|
|
356
|
+
onTransactionSuccess: viewModel.fetchData,
|
|
357
|
+
token: token,
|
|
358
|
+
),
|
|
359
|
+
),
|
|
360
|
+
],
|
|
361
|
+
),
|
|
362
|
+
const SizedBox(height: 16),
|
|
363
|
+
Row(
|
|
364
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
365
|
+
children: [
|
|
366
|
+
Expanded(
|
|
163
367
|
child: InvestmentForm(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
368
|
+
formKey: viewModel.investmentFormKey,
|
|
369
|
+
accountBalances: viewModel.accountBalances,
|
|
370
|
+
cashBalance: viewModel.appCalculatedCash,
|
|
371
|
+
onTransactionSuccess: viewModel.fetchData,
|
|
372
|
+
token: token,
|
|
373
|
+
),
|
|
374
|
+
),
|
|
375
|
+
const SizedBox(width: 16),
|
|
376
|
+
Expanded(
|
|
172
377
|
child: SalaryForm(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
378
|
+
formKey: viewModel.salaryFormKey,
|
|
379
|
+
onTransactionSuccess: viewModel.fetchData,
|
|
380
|
+
token: token,
|
|
381
|
+
),
|
|
382
|
+
),
|
|
383
|
+
],
|
|
384
|
+
),
|
|
385
|
+
],
|
|
386
|
+
),
|
|
180
387
|
),
|
|
181
388
|
),
|
|
182
389
|
floatingActionButton: FloatingActionButton(
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import 'package:shared_preferences/shared_preferences.dart';
|
|
2
|
+
|
|
3
|
+
/// Local persistence for dashboard widget order.
|
|
4
|
+
///
|
|
5
|
+
/// MVP: store a list of widget IDs in SharedPreferences.
|
|
6
|
+
/// If nothing is stored yet, we fall back to the default order.
|
|
7
|
+
class DashboardLayoutStorage {
|
|
8
|
+
static const _key = 'dashboard_widget_order_v1';
|
|
9
|
+
|
|
10
|
+
Future<List<String>?> loadOrder() async {
|
|
11
|
+
final prefs = await SharedPreferences.getInstance();
|
|
12
|
+
final list = prefs.getStringList(_key);
|
|
13
|
+
if (list == null || list.isEmpty) return null;
|
|
14
|
+
return list;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Future<void> saveOrder(List<String> ids) async {
|
|
18
|
+
final prefs = await SharedPreferences.getInstance();
|
|
19
|
+
await prefs.setStringList(_key, ids);
|
|
20
|
+
}
|
|
21
|
+
}
|
package/package.json
CHANGED