Agrego frontend app
This commit is contained in:
+445
@@ -0,0 +1,445 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:gimnasio_soma/core/theme/activity_colors.dart';
|
||||
import 'package:gimnasio_soma/core/theme/soma_colors.dart';
|
||||
import 'package:gimnasio_soma/core/widgets/soma_toast.dart';
|
||||
import 'package:gimnasio_soma/features/actividades/domain/entities/actividad.dart';
|
||||
import 'package:gimnasio_soma/features/actividades/presentation/providers/actividades_provider.dart';
|
||||
import 'package:gimnasio_soma/features/actividades/presentation/widgets/actividad_form_dialog.dart';
|
||||
|
||||
class ActividadesScreen extends ConsumerStatefulWidget {
|
||||
const ActividadesScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ActividadesScreen> createState() => _ActividadesScreenState();
|
||||
}
|
||||
|
||||
class _ActividadesScreenState extends ConsumerState<ActividadesScreen> {
|
||||
Future<void> _showCreateDialog() async {
|
||||
final result = await showDialog<Map<String, dynamic>>(
|
||||
context: context,
|
||||
builder: (_) => const ActividadFormDialog(),
|
||||
);
|
||||
if (result == null || !mounted) return;
|
||||
|
||||
final error =
|
||||
await ref.read(actividadesProvider.notifier).insertActividad(result);
|
||||
if (!mounted) return;
|
||||
if (error != null) {
|
||||
SomaToast.show(context, message: error, type: ToastType.error);
|
||||
} else {
|
||||
SomaToast.show(context,
|
||||
message: 'Actividad creada', type: ToastType.success);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showEditDialog(Actividad actividad) async {
|
||||
final result = await showDialog<Map<String, dynamic>>(
|
||||
context: context,
|
||||
builder: (_) => ActividadFormDialog(actividad: actividad),
|
||||
);
|
||||
if (result == null || !mounted) return;
|
||||
|
||||
final error = await ref
|
||||
.read(actividadesProvider.notifier)
|
||||
.updateActividad(actividad.id, result);
|
||||
if (!mounted) return;
|
||||
if (error != null) {
|
||||
SomaToast.show(context, message: error, type: ToastType.error);
|
||||
} else {
|
||||
SomaToast.show(context,
|
||||
message: 'Actividad actualizada', type: ToastType.success);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteActividad(Actividad actividad) async {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Eliminar actividad'),
|
||||
content: Text(
|
||||
'¿Estás seguro de que querés eliminar "${actividad.nombre}"?\n'
|
||||
'Esta acción no se puede deshacer.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text('Cancelar'),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: SomaColors.error,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(0, 40),
|
||||
),
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
child: const Text('Eliminar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirm != true || !mounted) return;
|
||||
|
||||
final error = await ref
|
||||
.read(actividadesProvider.notifier)
|
||||
.deleteActividad(actividad.id);
|
||||
if (!mounted) return;
|
||||
if (error != null) {
|
||||
SomaToast.show(context, message: error, type: ToastType.error);
|
||||
} else {
|
||||
SomaToast.show(context,
|
||||
message: 'Actividad eliminada', type: ToastType.success);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = ref.watch(actividadesProvider);
|
||||
final isWide = MediaQuery.of(context).size.width >= 800;
|
||||
|
||||
return Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
// Header
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
isWide ? 32 : 16,
|
||||
isWide ? 28 : 16,
|
||||
isWide ? 32 : 16,
|
||||
12,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text(
|
||||
'Actividades',
|
||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700),
|
||||
),
|
||||
const Spacer(),
|
||||
_AddButton(isWide: isWide, onTap: _showCreateDialog),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Lista
|
||||
Expanded(
|
||||
child: state.when(
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(color: SomaColors.primary),
|
||||
),
|
||||
error: (e, _) => Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.error_outline,
|
||||
size: 48,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withAlpha(100)),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
e.toString().replaceFirst('Exception: ', ''),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withAlpha(153),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextButton.icon(
|
||||
onPressed: () => ref
|
||||
.read(actividadesProvider.notifier)
|
||||
.loadActividades(),
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
label: const Text('Reintentar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
data: (actividades) {
|
||||
if (actividades.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.fitness_center_outlined,
|
||||
size: 56,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withAlpha(60)),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'No hay actividades',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withAlpha(130),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
color: SomaColors.primary,
|
||||
onRefresh: () => ref
|
||||
.read(actividadesProvider.notifier)
|
||||
.loadActividades(),
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
isWide ? 32 : 16, 4, isWide ? 32 : 16, 80,
|
||||
),
|
||||
itemCount: actividades.length,
|
||||
separatorBuilder: (_, _) => const SizedBox(height: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final act = actividades[index];
|
||||
return _ActividadCard(
|
||||
actividad: act,
|
||||
onEdit: () => _showEditDialog(act),
|
||||
onDelete: () => _deleteActividad(act),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActividadCard extends StatelessWidget {
|
||||
final Actividad actividad;
|
||||
final VoidCallback onEdit;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
const _ActividadCard({
|
||||
required this.actividad,
|
||||
required this.onEdit,
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return InkWell(
|
||||
onTap: onEdit,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: theme.colorScheme.surface,
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Ícono
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: actividad.activo
|
||||
? ActivityColors.forId(actividad.id).withAlpha(30)
|
||||
: theme.colorScheme.onSurface.withAlpha(15),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.fitness_center,
|
||||
size: 20,
|
||||
color: actividad.activo
|
||||
? theme.colorScheme.onSurface
|
||||
: theme.colorScheme.onSurface.withAlpha(100),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
|
||||
// Info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
actividad.nombre,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (!actividad.activo)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.onSurface.withAlpha(20),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
'Inactiva',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color:
|
||||
theme.colorScheme.onSurface.withAlpha(130),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (actividad.libre)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 6),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: SomaColors.success.withAlpha(20),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: const Text(
|
||||
'Libre',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: SomaColors.success,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.timer_outlined,
|
||||
size: 14,
|
||||
color:
|
||||
theme.colorScheme.onSurface.withAlpha(100)),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
actividad.duracionDisplay,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color:
|
||||
theme.colorScheme.onSurface.withAlpha(130),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
' • ',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color:
|
||||
theme.colorScheme.onSurface.withAlpha(80),
|
||||
),
|
||||
),
|
||||
Icon(Icons.group_outlined,
|
||||
size: 14,
|
||||
color:
|
||||
theme.colorScheme.onSurface.withAlpha(100)),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${actividad.capacidadPorDefecto} personas',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color:
|
||||
theme.colorScheme.onSurface.withAlpha(130),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Actions
|
||||
PopupMenuButton<String>(
|
||||
icon: Icon(
|
||||
Icons.more_vert,
|
||||
size: 18,
|
||||
color: theme.colorScheme.onSurface.withAlpha(100),
|
||||
),
|
||||
itemBuilder: (_) => [
|
||||
const PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.edit_outlined, size: 18),
|
||||
SizedBox(width: 8),
|
||||
Text('Editar'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.delete_outline, size: 18, color: SomaColors.error),
|
||||
SizedBox(width: 8),
|
||||
Text('Eliminar',
|
||||
style: TextStyle(color: SomaColors.error)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
onSelected: (v) {
|
||||
if (v == 'edit') onEdit();
|
||||
if (v == 'delete') onDelete();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AddButton extends StatelessWidget {
|
||||
final bool isWide;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _AddButton({required this.isWide, required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isWide) {
|
||||
return ElevatedButton.icon(
|
||||
onPressed: onTap,
|
||||
icon: const Icon(Icons.add, size: 20),
|
||||
label: const Text('Nueva'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(0, 42),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: 42,
|
||||
width: 42,
|
||||
child: IconButton.filled(
|
||||
onPressed: onTap,
|
||||
icon: const Icon(Icons.add, size: 22),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: SomaColors.primary,
|
||||
foregroundColor: SomaColors.onPrimary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user