@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 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 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: '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: 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(
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
- context,
117
- viewModel,
118
- 'RBC Balance',
119
- viewModel.rbcCardController,
120
- viewModel.rbcUnpaidExpenses)),
121
- const SizedBox(width: 16),
122
- Expanded(
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
- 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(
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
- 'Cash Transactions',
138
- viewModel.cashTransactions)),
139
- const SizedBox(width: 16),
140
- Expanded(
334
+ 'Cash Transactions',
335
+ viewModel.cashTransactions,
336
+ ),
337
+ ),
338
+ const SizedBox(width: 16),
339
+ Expanded(
141
340
  child: _buildRecentTransactionsList(
142
- 'RBC Transactions',
143
- viewModel.rbcTransactions)),
144
- const SizedBox(width: 16),
145
- Expanded(
341
+ 'RBC Transactions',
342
+ viewModel.rbcTransactions,
343
+ ),
344
+ ),
345
+ const SizedBox(width: 16),
346
+ Expanded(
146
347
  child: _buildRecentTransactionsList(
147
- 'BMO Transactions',
148
- viewModel.bmoTransactions)),
149
- const SizedBox(width: 16),
150
- Expanded(
348
+ 'BMO Transactions',
349
+ viewModel.bmoTransactions,
350
+ ),
351
+ ),
352
+ const SizedBox(width: 16),
353
+ Expanded(
151
354
  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(
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
- 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(
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
- formKey: viewModel.salaryFormKey,
174
- onTransactionSuccess: viewModel.fetchData,
175
- token: token,
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
@@ -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.1",
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: