@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 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 StatelessWidget {
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: Column(
105
- crossAxisAlignment: CrossAxisAlignment.start,
106
- children: [
107
- Row(
108
- crossAxisAlignment: CrossAxisAlignment.start,
109
- children: [
110
- Expanded(
111
- child: _buildCashBalanceCard(
112
- context, viewModel)),
113
- const SizedBox(width: 16),
114
- Expanded(
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
- context,
117
- viewModel,
118
- 'RBC Balance',
119
- viewModel.rbcCardController,
120
- viewModel.rbcUnpaidExpenses)),
121
- const SizedBox(width: 16),
122
- Expanded(
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
- context,
125
- viewModel,
126
- 'BMO Balance',
127
- viewModel.bmoCardController,
128
- viewModel.bmoUnpaidExpenses)),
129
- ],
130
- ),
131
- const SizedBox(height: 16),
132
- Row(
133
- crossAxisAlignment: CrossAxisAlignment.start,
134
- children: [
135
- Expanded(
136
- child: _buildRecentTransactionsList(
137
- 'Cash Transactions',
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
- formKey: viewModel.expenseFormKey,
153
- onTransactionSuccess: viewModel.fetchData,
154
- token: token,
155
- )),
156
- ],
157
- ),
158
- const SizedBox(height: 16),
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
- formKey: viewModel.investmentFormKey,
165
- accountBalances: viewModel.accountBalances,
166
- cashBalance: viewModel.appCalculatedCash,
167
- onTransactionSuccess: viewModel.fetchData,
168
- token: token,
169
- )),
170
- const SizedBox(width: 16),
171
- Expanded(
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
- formKey: viewModel.salaryFormKey,
174
- onTransactionSuccess: viewModel.fetchData,
175
- token: token,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marcos_feitoza/personal-finance-frontend-feature-dashboard",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/pubspec.yaml CHANGED
@@ -11,6 +11,7 @@ dependencies:
11
11
  sdk: flutter
12
12
  intl: ^0.17.0
13
13
  provider: ^6.0.0
14
+ shared_preferences: ^2.0.0
14
15
  personal_finance_frontend_core_services:
15
16
  path: ../personal-finance-frontend-core-services
16
17
  personal_finance_frontend_core_ui: