Agrego frontend app

This commit is contained in:
Pablo
2026-08-22 19:08:19 -03:00
parent 824e092b29
commit 2d0797e627
254 changed files with 37231 additions and 0 deletions
@@ -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,
),
),
);
}
}