187 lines
5.6 KiB
Dart
187 lines
5.6 KiB
Dart
|
|
import 'package:flutter/material.dart';
|
||
|
|
import 'package:go_router/go_router.dart';
|
||
|
|
|
||
|
|
class DashboardScreen extends StatelessWidget {
|
||
|
|
const DashboardScreen({super.key});
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
final cs = Theme.of(context).colorScheme;
|
||
|
|
final tt = Theme.of(context).textTheme;
|
||
|
|
|
||
|
|
return SingleChildScrollView(
|
||
|
|
padding: const EdgeInsets.all(32),
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
'Inicio',
|
||
|
|
style: tt.headlineMedium?.copyWith(fontWeight: FontWeight.bold),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 4),
|
||
|
|
Text(
|
||
|
|
'Accesos rápidos',
|
||
|
|
style: tt.bodyLarge?.copyWith(color: cs.onSurfaceVariant),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 32),
|
||
|
|
_QuickAccessGrid(),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Grid de accesos rápidos ───────────────────────────────────────────────
|
||
|
|
|
||
|
|
class _QuickAccessGrid extends StatelessWidget {
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
return LayoutBuilder(
|
||
|
|
builder: (context, constraints) {
|
||
|
|
final columns = constraints.maxWidth >= 900 ? 4 : 2;
|
||
|
|
final spacing = 16.0;
|
||
|
|
final cardWidth =
|
||
|
|
(constraints.maxWidth - spacing * (columns - 1)) / columns;
|
||
|
|
|
||
|
|
return Wrap(
|
||
|
|
spacing: spacing,
|
||
|
|
runSpacing: spacing,
|
||
|
|
children: _kShortcuts
|
||
|
|
.map((s) => SizedBox(
|
||
|
|
width: cardWidth,
|
||
|
|
child: _ShortcutCard(shortcut: s),
|
||
|
|
))
|
||
|
|
.toList(),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Tarjeta individual ────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
class _ShortcutCard extends StatelessWidget {
|
||
|
|
final _Shortcut shortcut;
|
||
|
|
|
||
|
|
const _ShortcutCard({required this.shortcut});
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
final cs = Theme.of(context).colorScheme;
|
||
|
|
final tt = Theme.of(context).textTheme;
|
||
|
|
|
||
|
|
final bgColor = shortcut.color.resolve(cs);
|
||
|
|
final fgColor = shortcut.foreground.resolve(cs);
|
||
|
|
|
||
|
|
return Card(
|
||
|
|
elevation: 0,
|
||
|
|
shape: RoundedRectangleBorder(
|
||
|
|
borderRadius: BorderRadius.circular(20),
|
||
|
|
side: BorderSide(color: cs.outlineVariant),
|
||
|
|
),
|
||
|
|
clipBehavior: Clip.antiAlias,
|
||
|
|
child: InkWell(
|
||
|
|
onTap: () => context.go(shortcut.route),
|
||
|
|
child: Padding(
|
||
|
|
padding: const EdgeInsets.all(24),
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
Container(
|
||
|
|
width: 52,
|
||
|
|
height: 52,
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: bgColor,
|
||
|
|
borderRadius: BorderRadius.circular(14),
|
||
|
|
),
|
||
|
|
child: Icon(shortcut.icon, color: fgColor, size: 26),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 20),
|
||
|
|
Text(
|
||
|
|
shortcut.title,
|
||
|
|
style: tt.titleMedium?.copyWith(fontWeight: FontWeight.w600),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 4),
|
||
|
|
Text(
|
||
|
|
shortcut.subtitle,
|
||
|
|
style: tt.bodySmall?.copyWith(color: cs.onSurfaceVariant),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Datos ─────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
class _Shortcut {
|
||
|
|
final String title;
|
||
|
|
final String subtitle;
|
||
|
|
final IconData icon;
|
||
|
|
final String route;
|
||
|
|
final _SchemeColor color;
|
||
|
|
final _SchemeColor foreground;
|
||
|
|
|
||
|
|
const _Shortcut({
|
||
|
|
required this.title,
|
||
|
|
required this.subtitle,
|
||
|
|
required this.icon,
|
||
|
|
required this.route,
|
||
|
|
required this.color,
|
||
|
|
required this.foreground,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Referencia indirecta a un color del ColorScheme para evitar context en const.
|
||
|
|
class _SchemeColor {
|
||
|
|
final Color Function(ColorScheme) resolve;
|
||
|
|
const _SchemeColor(this.resolve);
|
||
|
|
}
|
||
|
|
|
||
|
|
const _kShortcuts = [
|
||
|
|
_Shortcut(
|
||
|
|
title: 'Abonados',
|
||
|
|
subtitle: 'Gestionar abonados del club',
|
||
|
|
icon: Icons.people_outlined,
|
||
|
|
route: '/abonados',
|
||
|
|
color: _SchemeColor(_primary),
|
||
|
|
foreground: _SchemeColor(_onPrimary),
|
||
|
|
),
|
||
|
|
_Shortcut(
|
||
|
|
title: 'Reservas',
|
||
|
|
subtitle: 'Reservas de pistas',
|
||
|
|
icon: Icons.sports_tennis_outlined,
|
||
|
|
route: '/reservas',
|
||
|
|
color: _SchemeColor(_secondary),
|
||
|
|
foreground: _SchemeColor(_onSecondary),
|
||
|
|
),
|
||
|
|
_Shortcut(
|
||
|
|
title: 'Recibos',
|
||
|
|
subtitle: 'Recibos por abonado',
|
||
|
|
icon: Icons.receipt_long_outlined,
|
||
|
|
route: '/recibos/abonado',
|
||
|
|
color: _SchemeColor(_tertiary),
|
||
|
|
foreground: _SchemeColor(_onTertiary),
|
||
|
|
),
|
||
|
|
_Shortcut(
|
||
|
|
title: 'Caja',
|
||
|
|
subtitle: 'Movimientos y cobros',
|
||
|
|
icon: Icons.point_of_sale_outlined,
|
||
|
|
route: '/caja',
|
||
|
|
color: _SchemeColor(_error),
|
||
|
|
foreground: _SchemeColor(_onError),
|
||
|
|
),
|
||
|
|
];
|
||
|
|
|
||
|
|
// Funciones top-level para poder usarlas en const
|
||
|
|
Color _primary(ColorScheme cs) => cs.primaryContainer;
|
||
|
|
Color _onPrimary(ColorScheme cs) => cs.onPrimaryContainer;
|
||
|
|
Color _secondary(ColorScheme cs) => cs.secondaryContainer;
|
||
|
|
Color _onSecondary(ColorScheme cs) => cs.onSecondaryContainer;
|
||
|
|
Color _tertiary(ColorScheme cs) => cs.tertiaryContainer;
|
||
|
|
Color _onTertiary(ColorScheme cs) => cs.onTertiaryContainer;
|
||
|
|
Color _error(ColorScheme cs) => cs.errorContainer;
|
||
|
|
Color _onError(ColorScheme cs) => cs.onErrorContainer;
|