@marcos_feitoza/personal-finance-frontend-feature-dashboard 1.2.0 → 1.2.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.
- package/CHANGELOG.md +15 -0
- package/lib/models/dashboard_widget.dart +19 -0
- package/lib/screens/home_screen.dart +231 -72
- 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,18 @@
|
|
|
1
|
+
## [1.2.2](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/compare/v1.2.1...v1.2.2) (2026-02-01)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* remove Tops ([a9c5979](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/commit/a9c5979fbb4bf21378d39232b803b5ef85bff9fb))
|
|
7
|
+
|
|
8
|
+
## [1.2.1](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/compare/v1.2.0...v1.2.1) (2026-01-30)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* Reorder no mobile ([1342ce5](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/commit/1342ce51d9f0c1b4ea2fc684dfe756d1835b252e))
|
|
14
|
+
* Web responsivo (mobile/tablet/resize) ([9ad887a](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/commit/9ad887ae839e6bb70edfe2f50877f00dc36d4851))
|
|
15
|
+
|
|
1
16
|
# [1.2.0](https://github.com/MarcosOps/personal-finance-frontend-feature-dashboard/compare/v1.1.2...v1.2.0) (2026-01-29)
|
|
2
17
|
|
|
3
18
|
|
|
@@ -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,165 @@ 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: 'expense_form',
|
|
104
|
+
title: 'New Expense',
|
|
105
|
+
builder: (context) => ExpenseForm(
|
|
106
|
+
formKey: viewModel.expenseFormKey,
|
|
107
|
+
onTransactionSuccess: viewModel.fetchData,
|
|
108
|
+
token: token,
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
DashboardWidgetDefinition(
|
|
112
|
+
id: 'investment_form',
|
|
113
|
+
title: 'Move Money / Investment',
|
|
114
|
+
builder: (context) => InvestmentForm(
|
|
115
|
+
formKey: viewModel.investmentFormKey,
|
|
116
|
+
accountBalances: viewModel.accountBalances,
|
|
117
|
+
cashBalance: viewModel.appCalculatedCash,
|
|
118
|
+
onTransactionSuccess: viewModel.fetchData,
|
|
119
|
+
token: token,
|
|
120
|
+
),
|
|
121
|
+
),
|
|
122
|
+
DashboardWidgetDefinition(
|
|
123
|
+
id: 'salary_form',
|
|
124
|
+
title: 'New Salary',
|
|
125
|
+
builder: (context) => SalaryForm(
|
|
126
|
+
formKey: viewModel.salaryFormKey,
|
|
127
|
+
onTransactionSuccess: viewModel.fetchData,
|
|
128
|
+
token: token,
|
|
129
|
+
),
|
|
130
|
+
),
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
List<DashboardWidgetDefinition> _applyOrder(
|
|
135
|
+
List<DashboardWidgetDefinition> defaults,
|
|
136
|
+
List<String>? order,
|
|
137
|
+
) {
|
|
138
|
+
if (order == null || order.isEmpty) return defaults;
|
|
139
|
+
|
|
140
|
+
final byId = {for (final w in defaults) w.id: w};
|
|
141
|
+
final ordered = <DashboardWidgetDefinition>[];
|
|
142
|
+
|
|
143
|
+
// Keep only known ids
|
|
144
|
+
for (final id in order) {
|
|
145
|
+
final w = byId[id];
|
|
146
|
+
if (w != null) ordered.add(w);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Append new widgets that weren't in the stored order
|
|
150
|
+
for (final w in defaults) {
|
|
151
|
+
if (!ordered.any((x) => x.id == w.id)) {
|
|
152
|
+
ordered.add(w);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return ordered;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
Widget _buildReorderableMobileDashboard(
|
|
160
|
+
BuildContext context,
|
|
161
|
+
DashboardViewModel viewModel,
|
|
162
|
+
String? token,
|
|
163
|
+
) {
|
|
164
|
+
if (!_mobileOrderLoaded) {
|
|
165
|
+
return const Center(child: CircularProgressIndicator());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
final defaults = _defaultMobileWidgets(context, viewModel, token);
|
|
169
|
+
final widgets = _applyOrder(defaults, _mobileOrder);
|
|
170
|
+
|
|
171
|
+
// ReorderableListView must be scrollable itself.
|
|
172
|
+
// We are already inside a SingleChildScrollView in the Scaffold body.
|
|
173
|
+
// So we disable its scroll and let the parent handle scroll.
|
|
174
|
+
return ReorderableListView(
|
|
175
|
+
shrinkWrap: true,
|
|
176
|
+
physics: const NeverScrollableScrollPhysics(),
|
|
177
|
+
onReorder: (oldIndex, newIndex) async {
|
|
178
|
+
final list = List<DashboardWidgetDefinition>.from(widgets);
|
|
179
|
+
if (newIndex > oldIndex) newIndex -= 1;
|
|
180
|
+
final item = list.removeAt(oldIndex);
|
|
181
|
+
list.insert(newIndex, item);
|
|
182
|
+
|
|
183
|
+
final ids = list.map((w) => w.id).toList();
|
|
184
|
+
setState(() {
|
|
185
|
+
_mobileOrder = ids;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await _layoutStorage.saveOrder(ids);
|
|
189
|
+
},
|
|
190
|
+
children: [
|
|
191
|
+
for (final w in widgets)
|
|
192
|
+
Padding(
|
|
193
|
+
key: ValueKey(w.id),
|
|
194
|
+
padding: const EdgeInsets.only(bottom: 16.0),
|
|
195
|
+
child: w.builder(context),
|
|
196
|
+
),
|
|
197
|
+
],
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
41
201
|
@override
|
|
42
202
|
Widget build(BuildContext context) {
|
|
43
203
|
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
|
@@ -101,82 +261,81 @@ class HomeScreen extends StatelessWidget {
|
|
|
101
261
|
? const Center(child: CircularProgressIndicator())
|
|
102
262
|
: SingleChildScrollView(
|
|
103
263
|
padding: const EdgeInsets.all(16.0),
|
|
104
|
-
child:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
264
|
+
child: ResponsiveBuilder(
|
|
265
|
+
// Mobile: stack sections vertically (single column)
|
|
266
|
+
mobile: (context) => _buildReorderableMobileDashboard(
|
|
267
|
+
context,
|
|
268
|
+
viewModel,
|
|
269
|
+
token,
|
|
270
|
+
),
|
|
271
|
+
|
|
272
|
+
// Desktop (and tablet fallback): keep the wide dashboard layout.
|
|
273
|
+
desktop: (context) => Column(
|
|
274
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
275
|
+
children: [
|
|
276
|
+
Row(
|
|
277
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
278
|
+
children: [
|
|
279
|
+
Expanded(
|
|
280
|
+
child: _buildCashBalanceCard(context, viewModel),
|
|
281
|
+
),
|
|
282
|
+
const SizedBox(width: 16),
|
|
283
|
+
Expanded(
|
|
115
284
|
child: _buildCreditCardBalanceCard(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
285
|
+
context,
|
|
286
|
+
viewModel,
|
|
287
|
+
'RBC Balance',
|
|
288
|
+
viewModel.rbcCardController,
|
|
289
|
+
viewModel.rbcUnpaidExpenses,
|
|
290
|
+
),
|
|
291
|
+
),
|
|
292
|
+
const SizedBox(width: 16),
|
|
293
|
+
Expanded(
|
|
123
294
|
child: _buildCreditCardBalanceCard(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
viewModel.cashTransactions)),
|
|
139
|
-
const SizedBox(width: 16),
|
|
140
|
-
Expanded(
|
|
141
|
-
child: _buildRecentTransactionsList(
|
|
142
|
-
'RBC Transactions',
|
|
143
|
-
viewModel.rbcTransactions)),
|
|
144
|
-
const SizedBox(width: 16),
|
|
145
|
-
Expanded(
|
|
146
|
-
child: _buildRecentTransactionsList(
|
|
147
|
-
'BMO Transactions',
|
|
148
|
-
viewModel.bmoTransactions)),
|
|
149
|
-
const SizedBox(width: 16),
|
|
150
|
-
Expanded(
|
|
295
|
+
context,
|
|
296
|
+
viewModel,
|
|
297
|
+
'BMO Balance',
|
|
298
|
+
viewModel.bmoCardController,
|
|
299
|
+
viewModel.bmoUnpaidExpenses,
|
|
300
|
+
),
|
|
301
|
+
),
|
|
302
|
+
],
|
|
303
|
+
),
|
|
304
|
+
const SizedBox(height: 16),
|
|
305
|
+
Row(
|
|
306
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
307
|
+
children: [
|
|
308
|
+
Expanded(
|
|
151
309
|
child: ExpenseForm(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
Row(
|
|
160
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
161
|
-
children: [
|
|
162
|
-
Expanded(
|
|
310
|
+
formKey: viewModel.expenseFormKey,
|
|
311
|
+
onTransactionSuccess: viewModel.fetchData,
|
|
312
|
+
token: token,
|
|
313
|
+
),
|
|
314
|
+
),
|
|
315
|
+
const SizedBox(width: 16),
|
|
316
|
+
Expanded(
|
|
163
317
|
child: InvestmentForm(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
318
|
+
formKey: viewModel.investmentFormKey,
|
|
319
|
+
accountBalances: viewModel.accountBalances,
|
|
320
|
+
cashBalance: viewModel.appCalculatedCash,
|
|
321
|
+
onTransactionSuccess: viewModel.fetchData,
|
|
322
|
+
token: token,
|
|
323
|
+
),
|
|
324
|
+
),
|
|
325
|
+
const SizedBox(width: 16),
|
|
326
|
+
Expanded(
|
|
172
327
|
child: SalaryForm(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
328
|
+
formKey: viewModel.salaryFormKey,
|
|
329
|
+
onTransactionSuccess: viewModel.fetchData,
|
|
330
|
+
token: token,
|
|
331
|
+
),
|
|
332
|
+
),
|
|
333
|
+
],
|
|
334
|
+
),
|
|
335
|
+
const SizedBox(height: 16),
|
|
336
|
+
// Move Money + Salary are now on the same row as Add Expense.
|
|
337
|
+
],
|
|
338
|
+
),
|
|
180
339
|
),
|
|
181
340
|
),
|
|
182
341
|
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