Skip to content

Commit e864028

Browse files
committed
feat: Implementar vista de Dashboard con gestión de permisos y diseño adaptable
1 parent 6c9ac2c commit e864028

File tree

1 file changed

+376
-0
lines changed

1 file changed

+376
-0
lines changed
Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:boilerplate_frontend_mobile_flutter/app/services/permission_service.dart';
3+
import 'package:boilerplate_frontend_mobile_flutter/app/utils/responsive_layout.dart';
4+
5+
class DashboardView extends StatefulWidget {
6+
const DashboardView({super.key});
7+
8+
@override
9+
State<DashboardView> createState() => _DashboardViewState();
10+
}
11+
12+
class _DashboardViewState extends State<DashboardView> {
13+
final PermissionService _permissionService = PermissionService.instance;
14+
15+
// Métodos helper para verificar permisos
16+
bool hasPermission(String permission) => _permissionService.hasPermission(permission);
17+
bool canView(String module) => _permissionService.hasAnyPermission(['$module:index', '$module:view', '$module:show']);
18+
bool canCreate(String module) => _permissionService.canCreate(module);
19+
bool canEdit(String module) => _permissionService.canEdit(module);
20+
bool canDelete(String module) => _permissionService.canDelete(module);
21+
@override
22+
Widget build(BuildContext context) {
23+
return Scaffold(
24+
appBar: AppBar(
25+
title: Text(
26+
'Dashboard',
27+
style: TextStyle(
28+
fontSize: _getAppBarTextSize(context),
29+
fontWeight: FontWeight.w600,
30+
),
31+
),
32+
actions: [
33+
// Botón de configuraciones solo para administradores
34+
if (hasPermission('settings:access'))
35+
IconButton(
36+
icon: Icon(
37+
Icons.settings,
38+
size: _getIconSize(context),
39+
),
40+
onPressed: () => Navigator.pushNamed(context, 'settings'),
41+
),
42+
],
43+
toolbarHeight: _getAppBarHeight(context),
44+
),
45+
body: _buildResponsiveBody(context),
46+
floatingActionButton: _buildResponsiveFAB(context),
47+
);
48+
}
49+
50+
Widget _buildResponsiveBody(BuildContext context) {
51+
final padding = _getPadding(context);
52+
final crossAxisCount = _getCrossAxisCount(context);
53+
final childAspectRatio = _getChildAspectRatio(context);
54+
55+
return Padding(
56+
padding: padding,
57+
child: _buildGridOrList(context, crossAxisCount, childAspectRatio),
58+
);
59+
}
60+
61+
Widget _buildGridOrList(BuildContext context, int crossAxisCount, double childAspectRatio) {
62+
final availableModules = _buildAvailableModules();
63+
64+
if (Responsive.isMobile(context) && crossAxisCount == 1) {
65+
// En móvil con 1 columna, usar ListView para mejor scroll
66+
return ListView.separated(
67+
itemCount: availableModules.length,
68+
separatorBuilder: (context, index) => SizedBox(height: _getSpacing(context)),
69+
itemBuilder: (context, index) => availableModules[index],
70+
);
71+
} else {
72+
// En otros casos, usar GridView
73+
return GridView.count(
74+
crossAxisCount: crossAxisCount,
75+
crossAxisSpacing: _getSpacing(context),
76+
mainAxisSpacing: _getSpacing(context),
77+
childAspectRatio: childAspectRatio,
78+
children: availableModules,
79+
);
80+
}
81+
}
82+
83+
Widget? _buildResponsiveFAB(BuildContext context) {
84+
// En TV no mostrar FAB, en su lugar usar los botones en las cards
85+
if (Responsive.isTV(context)) return null;
86+
87+
final fabButtons = <Widget>[];
88+
89+
// FAB para crear usuario
90+
if (canCreate('users')) {
91+
fabButtons.add(
92+
FloatingActionButton(
93+
onPressed: () => Navigator.pushNamed(context, 'users_create'),
94+
tooltip: 'Crear Usuario',
95+
heroTag: "create_user",
96+
child: Icon(
97+
Icons.person_add,
98+
size: _getIconSize(context),
99+
),
100+
),
101+
);
102+
}
103+
104+
// FAB para crear rol
105+
if (canCreate('roles')) {
106+
fabButtons.add(
107+
FloatingActionButton(
108+
onPressed: () => Navigator.pushNamed(context, 'roles_create'),
109+
tooltip: 'Crear Rol',
110+
heroTag: "create_role",
111+
child: Icon(
112+
Icons.add_moderator,
113+
size: _getIconSize(context),
114+
),
115+
),
116+
);
117+
}
118+
119+
if (fabButtons.isEmpty) return null;
120+
121+
if (fabButtons.length == 1) {
122+
return fabButtons.first;
123+
}
124+
125+
// Múltiples FABs en columna
126+
return Column(
127+
mainAxisAlignment: MainAxisAlignment.end,
128+
children: fabButtons
129+
.expand((fab) => [fab, SizedBox(height: _getSpacing(context))])
130+
.take(fabButtons.length * 2 - 1)
131+
.toList(),
132+
);
133+
}
134+
135+
List<Widget> _buildAvailableModules() {
136+
List<Widget> modules = [];
137+
138+
// Card de Usuarios
139+
if (canView('users')) {
140+
modules.add(_buildModuleCard(
141+
title: 'Usuarios',
142+
icon: Icons.people,
143+
color: Colors.blue,
144+
onTap: () => Navigator.pushNamed(context, 'users_index'),
145+
actions: _buildUserActions(),
146+
));
147+
}
148+
149+
// Card de Roles
150+
if (canView('roles')) {
151+
modules.add(_buildModuleCard(
152+
title: 'Roles',
153+
icon: Icons.admin_panel_settings,
154+
color: Colors.green,
155+
onTap: () => Navigator.pushNamed(context, 'roles_index'),
156+
actions: _buildRoleActions(),
157+
));
158+
}
159+
160+
// Card de Reportes (ejemplo adicional)
161+
if (hasPermission('reports:view')) {
162+
modules.add(_buildModuleCard(
163+
title: 'Reportes',
164+
icon: Icons.analytics,
165+
color: Colors.orange,
166+
onTap: () => Navigator.pushNamed(context, 'reports_index'),
167+
actions: _buildReportActions(),
168+
));
169+
}
170+
171+
// Card de Configuraciones
172+
if (hasPermission('settings:access')) {
173+
modules.add(_buildModuleCard(
174+
title: 'Configuraciones',
175+
icon: Icons.settings,
176+
color: Colors.purple,
177+
onTap: () => Navigator.pushNamed(context, 'settings'),
178+
actions: _buildSettingsActions(),
179+
));
180+
}
181+
182+
return modules;
183+
}
184+
185+
Widget _buildModuleCard({
186+
required String title,
187+
required IconData icon,
188+
required Color color,
189+
required VoidCallback onTap,
190+
required List<Widget> actions,
191+
}) {
192+
return Card(
193+
elevation: 4,
194+
child: InkWell(
195+
onTap: onTap,
196+
child: Padding(
197+
padding: const EdgeInsets.all(16.0),
198+
child: Column(
199+
mainAxisAlignment: MainAxisAlignment.center,
200+
children: [
201+
Icon(
202+
icon,
203+
size: 48,
204+
color: color,
205+
),
206+
const SizedBox(height: 8),
207+
Text(
208+
title,
209+
style: Theme.of(context).textTheme.titleMedium?.copyWith(
210+
fontWeight: FontWeight.bold,
211+
),
212+
textAlign: TextAlign.center,
213+
),
214+
const SizedBox(height: 8),
215+
Wrap(
216+
spacing: 4,
217+
children: actions,
218+
),
219+
],
220+
),
221+
),
222+
),
223+
);
224+
}
225+
226+
List<Widget> _buildUserActions() {
227+
List<Widget> actions = [];
228+
229+
if (canView('users')) {
230+
actions.add(_buildActionChip('Ver', Icons.visibility, Colors.blue));
231+
}
232+
233+
if (canCreate('users')) {
234+
actions.add(_buildActionChip('Crear', Icons.add, Colors.green));
235+
}
236+
237+
if (canEdit('users')) {
238+
actions.add(_buildActionChip('Editar', Icons.edit, Colors.orange));
239+
}
240+
241+
if (canDelete('users')) {
242+
actions.add(_buildActionChip('Eliminar', Icons.delete, Colors.red));
243+
}
244+
245+
return actions;
246+
}
247+
248+
List<Widget> _buildRoleActions() {
249+
List<Widget> actions = [];
250+
251+
if (canView('roles')) {
252+
actions.add(_buildActionChip('Ver', Icons.visibility, Colors.blue));
253+
}
254+
255+
if (canCreate('roles')) {
256+
actions.add(_buildActionChip('Crear', Icons.add, Colors.green));
257+
}
258+
259+
if (canEdit('roles')) {
260+
actions.add(_buildActionChip('Editar', Icons.edit, Colors.orange));
261+
}
262+
263+
if (canDelete('roles')) {
264+
actions.add(_buildActionChip('Eliminar', Icons.delete, Colors.red));
265+
}
266+
267+
return actions;
268+
}
269+
270+
List<Widget> _buildReportActions() {
271+
List<Widget> actions = [];
272+
273+
if (hasPermission('reports:view')) {
274+
actions.add(_buildActionChip('Ver', Icons.visibility, Colors.blue));
275+
}
276+
277+
if (hasPermission('reports:export')) {
278+
actions.add(_buildActionChip('Exportar', Icons.download, Colors.green));
279+
}
280+
281+
return actions;
282+
}
283+
284+
List<Widget> _buildSettingsActions() {
285+
List<Widget> actions = [];
286+
287+
if (hasPermission('settings:access')) {
288+
actions.add(_buildActionChip('Configurar', Icons.settings, Colors.purple));
289+
}
290+
291+
return actions;
292+
}
293+
294+
Widget _buildActionChip(String label, IconData icon, Color color) {
295+
return Chip(
296+
label: Text(
297+
label,
298+
style: TextStyle(
299+
color: color,
300+
fontSize: 10,
301+
),
302+
),
303+
avatar: Icon(
304+
icon,
305+
size: 14,
306+
color: color,
307+
),
308+
backgroundColor: color.withOpacity(0.1),
309+
side: BorderSide(color: color.withOpacity(0.3)),
310+
);
311+
}
312+
313+
// Métodos para obtener tamaños responsive
314+
int _getCrossAxisCount(BuildContext context) {
315+
if (Responsive.isMobile(context)) return 1;
316+
if (Responsive.isTablet(context)) return 2;
317+
if (Responsive.isDesktop(context)) return 3;
318+
if (Responsive.isLargeDesktop(context)) return 4;
319+
if (Responsive.isTV(context)) return 5;
320+
return 2;
321+
}
322+
323+
double _getChildAspectRatio(BuildContext context) {
324+
if (Responsive.isMobile(context)) return 1.5;
325+
if (Responsive.isTablet(context)) return 1.3;
326+
if (Responsive.isDesktop(context)) return 1.2;
327+
if (Responsive.isLargeDesktop(context)) return 1.1;
328+
if (Responsive.isTV(context)) return 1.0;
329+
return 1.2;
330+
}
331+
332+
EdgeInsets _getPadding(BuildContext context) {
333+
if (Responsive.isMobile(context)) return const EdgeInsets.all(8.0);
334+
if (Responsive.isTablet(context)) return const EdgeInsets.all(16.0);
335+
if (Responsive.isDesktop(context)) return const EdgeInsets.all(24.0);
336+
if (Responsive.isLargeDesktop(context)) return const EdgeInsets.all(32.0);
337+
if (Responsive.isTV(context)) return const EdgeInsets.all(40.0);
338+
return const EdgeInsets.all(16.0);
339+
}
340+
341+
double _getSpacing(BuildContext context) {
342+
if (Responsive.isMobile(context)) return 8.0;
343+
if (Responsive.isTablet(context)) return 12.0;
344+
if (Responsive.isDesktop(context)) return 16.0;
345+
if (Responsive.isLargeDesktop(context)) return 20.0;
346+
if (Responsive.isTV(context)) return 24.0;
347+
return 12.0;
348+
}
349+
350+
double _getIconSize(BuildContext context) {
351+
if (Responsive.isMobile(context)) return 24.0;
352+
if (Responsive.isTablet(context)) return 28.0;
353+
if (Responsive.isDesktop(context)) return 32.0;
354+
if (Responsive.isLargeDesktop(context)) return 36.0;
355+
if (Responsive.isTV(context)) return 40.0;
356+
return 24.0;
357+
}
358+
359+
double _getAppBarHeight(BuildContext context) {
360+
if (Responsive.isMobile(context)) return 56.0;
361+
if (Responsive.isTablet(context)) return 64.0;
362+
if (Responsive.isDesktop(context)) return 72.0;
363+
if (Responsive.isLargeDesktop(context)) return 80.0;
364+
if (Responsive.isTV(context)) return 88.0;
365+
return 56.0;
366+
}
367+
368+
double _getAppBarTextSize(BuildContext context) {
369+
if (Responsive.isMobile(context)) return 20.0;
370+
if (Responsive.isTablet(context)) return 22.0;
371+
if (Responsive.isDesktop(context)) return 24.0;
372+
if (Responsive.isLargeDesktop(context)) return 26.0;
373+
if (Responsive.isTV(context)) return 28.0;
374+
return 20.0;
375+
}
376+
}

0 commit comments

Comments
 (0)