Resolve merge conflicts: README + ignore IDE files

This commit is contained in:
David
2026-05-23 07:11:33 -06:00
parent abfbb255fe
commit 6ff72c738d
27 changed files with 2123 additions and 335 deletions

View File

@@ -1,11 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../app_config.dart';
import '../models/demo_profile.dart';
import '../models/auth_session.dart';
import '../services/address_repository.dart';
import '../services/auth_repository.dart';
import '../services/local_seed_repository.dart';
import 'address_screen.dart';
final RegExp _lettersOnly = RegExp(r"[a-zA-ZáéíóúÁÉÍÓÚñÑüÜ\s]");
final RegExp _addressText = RegExp(r"[a-zA-Z0-9áéíóúÁÉÍÓÚñÑüÜ#\-\s]");
final RegExp _emailChars = RegExp(r"[a-zA-Z0-9@._+\-]");
class AuthScreen extends StatefulWidget {
const AuthScreen({
super.key,
@@ -34,6 +40,14 @@ class _AuthScreenState extends State<AuthScreen> {
bool _isLoading = false;
String? _errorMessage;
LocalSeedData? _seedData;
bool _loadingSeedData = true;
@override
void initState() {
super.initState();
_loadSeedData();
}
@override
void dispose() {
@@ -46,8 +60,42 @@ class _AuthScreenState extends State<AuthScreen> {
super.dispose();
}
Future<void> _loadSeedData() async {
final seedData = await LocalSeedRepository.instance.load();
if (!mounted) {
return;
}
setState(() {
_seedData = seedData;
_loadingSeedData = false;
});
}
void _fillDemoProfile(DemoProfile profile) {
_loginEmailController.text = profile.email;
_loginPasswordController.text = profile.password;
_registerNameController.text = profile.name;
_registerEmailController.text = profile.email;
_registerPasswordController.text = profile.password;
_registerConfirmPasswordController.text = profile.password;
}
Future<void> _useDemoProfile(DemoProfile profile) async {
_fillDemoProfile(profile);
await _submit(() {
return widget.authRepository.signIn(
email: profile.email,
password: profile.password,
);
});
}
Future<void> _signIn() async {
if (!(_loginFormKey.currentState?.validate() ?? false)) {
setState(() {
_errorMessage = 'Respete los campos';
});
return;
}
@@ -61,6 +109,9 @@ class _AuthScreenState extends State<AuthScreen> {
Future<void> _signUp() async {
if (!(_registerFormKey.currentState?.validate() ?? false)) {
setState(() {
_errorMessage = 'Respete los campos';
});
return;
}
@@ -106,7 +157,7 @@ class _AuthScreenState extends State<AuthScreen> {
return;
}
setState(() {
_errorMessage = 'No se pudo completar la operación. Verifica el backend y vuelve a intentar.';
_errorMessage = 'No se pudo completar la operación. Revisa los datos locales y vuelve a intentar.';
});
} finally {
if (mounted) {
@@ -167,6 +218,13 @@ class _AuthScreenState extends State<AuthScreen> {
style: TextStyle(color: Colors.grey.shade700, height: 1.4),
),
const SizedBox(height: 20),
if (!_loadingSeedData && _seedData != null && _seedData!.demoProfiles.isNotEmpty) ...[
_DemoProfilesSection(
profiles: _seedData!.demoProfiles,
onProfileSelected: _useDemoProfile,
),
const SizedBox(height: 16),
],
Container(
decoration: BoxDecoration(
color: const Color(0xFFF1F5F9),
@@ -221,11 +279,6 @@ class _AuthScreenState extends State<AuthScreen> {
),
),
const SizedBox(height: 12),
Text(
'Base URL configurada: ${AppConfig.apiBaseUrl}',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
textAlign: TextAlign.center,
),
],
),
),
@@ -264,6 +317,7 @@ class _LoginForm extends StatelessWidget {
TextFormField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
inputFormatters: [FilteringTextInputFormatter.allow(_emailChars)],
decoration: const InputDecoration(
labelText: 'Correo electrónico',
prefixIcon: Icon(Icons.email_outlined),
@@ -273,7 +327,7 @@ class _LoginForm extends StatelessWidget {
if (value == null || value.trim().isEmpty) {
return 'Ingresa tu correo';
}
if (!value.contains('@')) {
if (!value.contains('@') || value.startsWith('@') || value.endsWith('@')) {
return 'Ingresa un correo válido';
}
return null;
@@ -347,6 +401,8 @@ class _RegisterForm extends StatelessWidget {
TextFormField(
controller: nameController,
textCapitalization: TextCapitalization.words,
keyboardType: TextInputType.name,
inputFormatters: [FilteringTextInputFormatter.allow(_lettersOnly)],
decoration: const InputDecoration(
labelText: 'Nombre',
prefixIcon: Icon(Icons.person_outline),
@@ -363,6 +419,7 @@ class _RegisterForm extends StatelessWidget {
TextFormField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
inputFormatters: [FilteringTextInputFormatter.allow(_emailChars)],
decoration: const InputDecoration(
labelText: 'Correo electrónico',
prefixIcon: Icon(Icons.email_outlined),
@@ -372,7 +429,7 @@ class _RegisterForm extends StatelessWidget {
if (value == null || value.trim().isEmpty) {
return 'Ingresa tu correo';
}
if (!value.contains('@')) {
if (!value.contains('@') || value.startsWith('@') || value.endsWith('@')) {
return 'Ingresa un correo válido';
}
return null;
@@ -382,6 +439,7 @@ class _RegisterForm extends StatelessWidget {
TextFormField(
controller: passwordController,
obscureText: true,
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'\s'))],
decoration: const InputDecoration(
labelText: 'Contraseña',
prefixIcon: Icon(Icons.lock_outline),
@@ -401,6 +459,7 @@ class _RegisterForm extends StatelessWidget {
TextFormField(
controller: confirmPasswordController,
obscureText: true,
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'\s'))],
decoration: const InputDecoration(
labelText: 'Confirmar contraseña',
prefixIcon: Icon(Icons.lock_reset_outlined),
@@ -458,4 +517,47 @@ class _AuthStatusBanner extends StatelessWidget {
),
);
}
}
class _DemoProfilesSection extends StatelessWidget {
const _DemoProfilesSection({
required this.profiles,
required this.onProfileSelected,
});
final List<DemoProfile> profiles;
final Future<void> Function(DemoProfile profile) onProfileSelected;
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
color: const Color(0xFFF8FAFC),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Perfiles demo', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w800)),
const SizedBox(height: 8),
Text('Toca un perfil para llenar el formulario de acceso.', style: TextStyle(color: Colors.grey.shade700)),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: profiles
.map(
(profile) => ActionChip(
label: Text('${profile.name}${profile.routeId}'),
onPressed: () => onProfileSelected(profile),
),
)
.toList(growable: false),
),
],
),
),
);
}
}