Agrego frontend app
This commit is contained in:
+218
@@ -0,0 +1,218 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gimnasio_soma/core/theme/soma_colors.dart';
|
||||
import 'package:gimnasio_soma/core/widgets/soma_text_field.dart';
|
||||
import 'package:gimnasio_soma/features/actividades/domain/entities/actividad.dart';
|
||||
|
||||
class ActividadFormDialog extends StatefulWidget {
|
||||
final Actividad? actividad;
|
||||
|
||||
const ActividadFormDialog({super.key, this.actividad});
|
||||
|
||||
@override
|
||||
State<ActividadFormDialog> createState() => _ActividadFormDialogState();
|
||||
}
|
||||
|
||||
class _ActividadFormDialogState extends State<ActividadFormDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late final TextEditingController _nombreCtrl;
|
||||
late final TextEditingController _duracionCtrl;
|
||||
late final TextEditingController _capacidadCtrl;
|
||||
late bool _libre;
|
||||
|
||||
bool get _isEditing => widget.actividad != null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nombreCtrl = TextEditingController(text: widget.actividad?.nombre ?? '');
|
||||
_duracionCtrl = TextEditingController(
|
||||
text: widget.actividad != null ? widget.actividad!.duracion.toString() : '',
|
||||
);
|
||||
_capacidadCtrl = TextEditingController(
|
||||
text: widget.actividad != null
|
||||
? widget.actividad!.capacidadPorDefecto.toString()
|
||||
: '',
|
||||
);
|
||||
_libre = widget.actividad?.libre ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nombreCtrl.dispose();
|
||||
_duracionCtrl.dispose();
|
||||
_capacidadCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
final data = <String, dynamic>{
|
||||
'nombre': _nombreCtrl.text.trim(),
|
||||
'duracion': int.tryParse(_duracionCtrl.text.trim()) ?? 0,
|
||||
'capacidad_por_defecto': int.tryParse(_capacidadCtrl.text.trim()) ?? 0,
|
||||
'libre': _libre,
|
||||
};
|
||||
|
||||
Navigator.of(context).pop(data);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final isWide = width >= 600;
|
||||
|
||||
return Dialog(
|
||||
insetPadding: EdgeInsets.symmetric(
|
||||
horizontal: isWide ? (width - 440) / 2 : 20,
|
||||
vertical: 24,
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 440),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 20, 16, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
_isEditing ? 'Editar Actividad' : 'Nueva Actividad',
|
||||
style: const TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.w700),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 20),
|
||||
|
||||
// Form
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 8),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SomaTextField(
|
||||
controller: _nombreCtrl,
|
||||
labelText: 'Nombre *',
|
||||
prefixIcon: Icons.fitness_center,
|
||||
validator: (v) => v == null || v.trim().isEmpty
|
||||
? 'Nombre requerido'
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SomaTextField(
|
||||
controller: _duracionCtrl,
|
||||
labelText: 'Duración (min) *',
|
||||
prefixIcon: Icons.timer_outlined,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(4),
|
||||
],
|
||||
validator: (v) {
|
||||
if (v == null || v.trim().isEmpty) {
|
||||
return 'Requerido';
|
||||
}
|
||||
final n = int.tryParse(v.trim());
|
||||
if (n == null || n <= 0) return 'Inválido';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: SomaTextField(
|
||||
controller: _capacidadCtrl,
|
||||
labelText: 'Capacidad *',
|
||||
prefixIcon: Icons.group_outlined,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(4),
|
||||
],
|
||||
validator: (v) {
|
||||
if (v == null || v.trim().isEmpty) {
|
||||
return 'Requerido';
|
||||
}
|
||||
final n = int.tryParse(v.trim());
|
||||
if (n == null || n <= 0) return 'Inválido';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
title: const Text(
|
||||
'Actividad libre',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
subtitle: const Text(
|
||||
'No requiere turno previo',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
value: _libre,
|
||||
activeThumbColor: SomaColors.primary,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
onChanged: (v) => setState(() => _libre = v),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(height: 1),
|
||||
|
||||
// Actions
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 12, 24, 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(
|
||||
'Cancelar',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withAlpha(178),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ElevatedButton(
|
||||
onPressed: _submit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(0, 42),
|
||||
),
|
||||
child: Text(_isEditing ? 'Guardar' : 'Crear'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user