Agrego frontend app
This commit is contained in:
+155
@@ -0,0 +1,155 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:gimnasio_soma/core/widgets/soma_text_field.dart';
|
||||
import 'package:gimnasio_soma/core/widgets/soma_primary_button.dart';
|
||||
import 'package:gimnasio_soma/core/widgets/soma_toast.dart';
|
||||
import 'package:gimnasio_soma/features/auth/presentation/providers/auth_provider.dart';
|
||||
|
||||
/// Autoservicio: el propio usuario logueado cambia su contraseña,
|
||||
/// confirmando primero la actual. Accesible desde Perfil para admin/superadmin.
|
||||
class CambiarContrasenaScreen extends ConsumerStatefulWidget {
|
||||
const CambiarContrasenaScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<CambiarContrasenaScreen> createState() =>
|
||||
_CambiarContrasenaScreenState();
|
||||
}
|
||||
|
||||
class _CambiarContrasenaScreenState
|
||||
extends ConsumerState<CambiarContrasenaScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _actualCtrl = TextEditingController();
|
||||
final _nuevaCtrl = TextEditingController();
|
||||
final _repetirCtrl = TextEditingController();
|
||||
|
||||
bool _obscureActual = true;
|
||||
bool _obscureNueva = true;
|
||||
bool _obscureRepetir = true;
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_actualCtrl.dispose();
|
||||
_nuevaCtrl.dispose();
|
||||
_repetirCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
await ref.read(authRepositoryProvider).cambiarPropiaContrasena(
|
||||
passwordActual: _actualCtrl.text,
|
||||
passwordNueva: _nuevaCtrl.text,
|
||||
);
|
||||
if (!mounted) return;
|
||||
SomaToast.show(context,
|
||||
message: 'Contraseña actualizada', type: ToastType.success);
|
||||
context.pop();
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
SomaToast.show(
|
||||
context,
|
||||
message: e.toString().replaceFirst('Exception: ', ''),
|
||||
type: ToastType.error,
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _passwordField({
|
||||
required TextEditingController controller,
|
||||
required String label,
|
||||
required bool obscure,
|
||||
required VoidCallback onToggle,
|
||||
String? Function(String?)? validator,
|
||||
}) {
|
||||
return SomaTextField(
|
||||
controller: controller,
|
||||
labelText: label,
|
||||
prefixIcon: Icons.lock_outline,
|
||||
obscureText: obscure,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
obscure ? Icons.visibility_outlined : Icons.visibility_off_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurface.withAlpha(130),
|
||||
size: 20,
|
||||
),
|
||||
onPressed: onToggle,
|
||||
),
|
||||
validator: validator,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Cambiar contraseña')),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 420),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_passwordField(
|
||||
controller: _actualCtrl,
|
||||
label: 'Contraseña actual',
|
||||
obscure: _obscureActual,
|
||||
onToggle: () =>
|
||||
setState(() => _obscureActual = !_obscureActual),
|
||||
validator: (v) => (v == null || v.isEmpty)
|
||||
? 'Ingresá tu contraseña actual'
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_passwordField(
|
||||
controller: _nuevaCtrl,
|
||||
label: 'Contraseña nueva',
|
||||
obscure: _obscureNueva,
|
||||
onToggle: () =>
|
||||
setState(() => _obscureNueva = !_obscureNueva),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) {
|
||||
return 'Ingresá la contraseña nueva';
|
||||
}
|
||||
if (v.length < 8) return 'Mínimo 8 caracteres';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_passwordField(
|
||||
controller: _repetirCtrl,
|
||||
label: 'Repetir contraseña nueva',
|
||||
obscure: _obscureRepetir,
|
||||
onToggle: () =>
|
||||
setState(() => _obscureRepetir = !_obscureRepetir),
|
||||
validator: (v) {
|
||||
if (v != _nuevaCtrl.text) {
|
||||
return 'Las contraseñas no coinciden';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
SomaPrimaryButton(
|
||||
text: 'Guardar',
|
||||
onPressed: _submit,
|
||||
isLoading: _isLoading,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:gimnasio_soma/core/theme/soma_colors.dart';
|
||||
import 'package:gimnasio_soma/features/auth/presentation/providers/auth_provider.dart';
|
||||
|
||||
class PerfilScreen extends ConsumerWidget {
|
||||
const PerfilScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final user = ref.watch(authStateProvider).valueOrNull;
|
||||
final isWide = MediaQuery.of(context).size.width >= 800;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
if (user == null) return const SizedBox.shrink();
|
||||
|
||||
String initials() {
|
||||
if (user.nombre.isNotEmpty && user.apellido.isNotEmpty) {
|
||||
return '${user.nombre[0]}${user.apellido[0]}'.toUpperCase();
|
||||
}
|
||||
if (user.nombre.isNotEmpty) return user.nombre[0].toUpperCase();
|
||||
if (user.dni.length >= 2) return user.dni.substring(0, 2);
|
||||
return '?';
|
||||
}
|
||||
|
||||
String rolDisplay() {
|
||||
switch (user.role) {
|
||||
case 'superadmin':
|
||||
return 'Super Admin';
|
||||
case 'admin':
|
||||
return 'Administrador';
|
||||
case 'profesor':
|
||||
return 'Profesor';
|
||||
case 'cliente':
|
||||
return 'Cliente';
|
||||
default:
|
||||
return user.role;
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: SingleChildScrollView(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
isWide ? 32 : 16,
|
||||
isWide ? 28 : 16,
|
||||
isWide ? 32 : 16,
|
||||
32,
|
||||
),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
const Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
'Mi Perfil',
|
||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Avatar con ring amarillo
|
||||
Container(
|
||||
padding: const EdgeInsets.all(3),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: SomaColors.primary,
|
||||
width: 2.5,
|
||||
),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 40,
|
||||
backgroundColor: SomaColors.primary.withAlpha(35),
|
||||
child: Text(
|
||||
initials(),
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
user.displayName,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: SomaColors.primary.withAlpha(18),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: SomaColors.primary.withAlpha(60),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
rolDisplay(),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: SomaColors.primaryText,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
|
||||
// Info cards
|
||||
_InfoTile(
|
||||
icon: Icons.badge_outlined,
|
||||
label: 'DNI',
|
||||
value: user.dni,
|
||||
),
|
||||
if (user.mail != null && user.mail!.isNotEmpty)
|
||||
_InfoTile(
|
||||
icon: Icons.email_outlined,
|
||||
label: 'Email',
|
||||
value: user.mail!,
|
||||
),
|
||||
if (user.telefono != null && user.telefono!.isNotEmpty)
|
||||
_InfoTile(
|
||||
icon: Icons.phone_outlined,
|
||||
label: 'Teléfono',
|
||||
value: user.telefono!,
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Cambiar contraseña (staff)
|
||||
if (user.isStaff)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: theme.colorScheme.surface,
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
Icons.lock_outline,
|
||||
size: 20,
|
||||
color: theme.colorScheme.onSurface.withAlpha(153),
|
||||
),
|
||||
title: const Text(
|
||||
'Cambiar contraseña',
|
||||
style: TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right,
|
||||
size: 20,
|
||||
color: theme.colorScheme.onSurface.withAlpha(100),
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
onTap: () => context.go('/perfil/cambiar-contrasena'),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Logs (admin)
|
||||
if (user.isStaff)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: theme.colorScheme.surface,
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
Icons.terminal,
|
||||
size: 20,
|
||||
color:
|
||||
theme.colorScheme.onSurface.withAlpha(153),
|
||||
),
|
||||
title: const Text(
|
||||
'Ver logs',
|
||||
style: TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right,
|
||||
size: 20,
|
||||
color:
|
||||
theme.colorScheme.onSurface.withAlpha(100),
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
onTap: () => context.go('/logs'),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Logout
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () =>
|
||||
ref.read(authStateProvider.notifier).logout(),
|
||||
icon: const Icon(Icons.logout, size: 18),
|
||||
label: const Text('Cerrar sesión'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(0, 48),
|
||||
foregroundColor: SomaColors.error,
|
||||
side: const BorderSide(
|
||||
color: SomaColors.error, width: 0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoTile extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
const _InfoTile({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: theme.colorScheme.surface,
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: SomaColors.primary.withAlpha(16),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 18,
|
||||
color: SomaColors.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: theme.colorScheme.onSurface.withAlpha(110),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user