|
| 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