import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../providers/auth_provider.dart'; import '../../providers/tickets_provider.dart'; import '../../theme/m3_motion.dart'; import '../../utils/snackbar.dart'; class SignUpScreen extends ConsumerStatefulWidget { const SignUpScreen({super.key}); @override ConsumerState createState() => _SignUpScreenState(); } class _SignUpScreenState extends ConsumerState with SingleTickerProviderStateMixin { final _formKey = GlobalKey(); final _fullNameController = TextEditingController(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _confirmPasswordController = TextEditingController(); late final AnimationController _entranceController; late final Animation _fadeIn; late final Animation _slideIn; final Set _selectedOfficeIds = {}; double _passwordStrength = 0.0; String _passwordStrengthLabel = 'Very weak'; Color _passwordStrengthColor = Colors.red; bool _isLoading = false; bool _obscurePassword = true; bool _obscureConfirm = true; @override void initState() { super.initState(); _passwordController.addListener(_updatePasswordStrength); _entranceController = AnimationController( vsync: this, duration: M3Motion.long, ); _fadeIn = CurvedAnimation( parent: _entranceController, curve: M3Motion.emphasizedEnter, ); _slideIn = Tween( begin: const Offset(0, 0.06), end: Offset.zero, ).animate(_fadeIn); _entranceController.forward(); } @override void dispose() { _entranceController.dispose(); _passwordController.removeListener(_updatePasswordStrength); _fullNameController.dispose(); _emailController.dispose(); _passwordController.dispose(); _confirmPasswordController.dispose(); super.dispose(); } Future _handleSignUp() async { if (!_formKey.currentState!.validate()) return; if (_selectedOfficeIds.isEmpty) { showWarningSnackBar(context, 'Select at least one office.'); return; } setState(() => _isLoading = true); final auth = ref.read(authControllerProvider); try { await auth.signUp( email: _emailController.text.trim(), password: _passwordController.text, fullName: _fullNameController.text.trim(), officeIds: _selectedOfficeIds.toList(), ); if (mounted) { context.go('/login'); } } on Exception catch (error) { if (mounted) { showErrorSnackBar(context, 'Sign up failed: $error'); } } finally { if (mounted) { setState(() => _isLoading = false); } } } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final tt = Theme.of(context).textTheme; final officesAsync = ref.watch(officesOnceProvider); return Scaffold( body: SafeArea( child: Center( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), child: FadeTransition( opacity: _fadeIn, child: SlideTransition( position: _slideIn, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 420), child: Column( mainAxisSize: MainAxisSize.min, children: [ // ── Branding ── Hero( tag: 'tasq-logo', child: Image.asset( 'assets/tasq_ico.png', height: 80, width: 80, ), ), const SizedBox(height: 16), Text( 'TasQ', style: tt.headlineMedium?.copyWith( fontWeight: FontWeight.w700, color: cs.primary, ), ), const SizedBox(height: 4), Text( 'Create your account', style: tt.bodyMedium?.copyWith( color: cs.onSurfaceVariant, ), ), const SizedBox(height: 32), // ── Sign-up card ── Card( elevation: 0, color: cs.surfaceContainerLow, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(28), ), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 28, ), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( 'Sign up', style: tt.titleLarge?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 20), TextFormField( controller: _fullNameController, decoration: const InputDecoration( labelText: 'Full name', prefixIcon: Icon(Icons.person_outlined), ), textInputAction: TextInputAction.next, validator: (value) { if (value == null || value.trim().isEmpty) { return 'Full name is required.'; } return null; }, ), const SizedBox(height: 16), TextFormField( controller: _emailController, decoration: const InputDecoration( labelText: 'Email', prefixIcon: Icon(Icons.email_outlined), ), keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, validator: (value) { if (value == null || value.trim().isEmpty) { return 'Email is required.'; } return null; }, ), const SizedBox(height: 16), TextFormField( controller: _passwordController, decoration: InputDecoration( labelText: 'Password', prefixIcon: const Icon(Icons.lock_outlined), suffixIcon: IconButton( icon: Icon( _obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, ), onPressed: () => setState( () => _obscurePassword = !_obscurePassword, ), ), ), obscureText: _obscurePassword, textInputAction: TextInputAction.next, validator: (value) { if (value == null || value.isEmpty) { return 'Password is required.'; } if (value.length < 6) { return 'Use at least 6 characters.'; } return null; }, ), const SizedBox(height: 8), // ── Password strength (AnimatedSize) ── AnimatedSize( duration: M3Motion.short, curve: M3Motion.standard_, child: _passwordController.text.isEmpty ? const SizedBox.shrink() : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( 'Strength: ', style: tt.labelSmall ?.copyWith( color: cs.onSurfaceVariant, ), ), Text( _passwordStrengthLabel, style: tt.labelSmall?.copyWith( color: _passwordStrengthColor, fontWeight: FontWeight.w600, ), ), ], ), const SizedBox(height: 6), ClipRRect( borderRadius: BorderRadius.circular(8), child: LinearProgressIndicator( value: _passwordStrength, minHeight: 6, color: _passwordStrengthColor, backgroundColor: cs.surfaceContainerHighest, ), ), const SizedBox(height: 8), ], ), ), TextFormField( controller: _confirmPasswordController, decoration: InputDecoration( labelText: 'Confirm password', prefixIcon: const Icon(Icons.lock_outlined), suffixIcon: IconButton( icon: Icon( _obscureConfirm ? Icons.visibility_outlined : Icons.visibility_off_outlined, ), onPressed: () => setState( () => _obscureConfirm = !_obscureConfirm, ), ), ), obscureText: _obscureConfirm, textInputAction: TextInputAction.done, onFieldSubmitted: (_) { if (!_isLoading) _handleSignUp(); }, validator: (value) { if (value == null || value.isEmpty) { return 'Confirm your password.'; } if (value != _passwordController.text) { return 'Passwords do not match.'; } return null; }, ), const SizedBox(height: 20), // ── Office selection ── Text('Offices', style: tt.titleSmall), const SizedBox(height: 8), officesAsync.when( data: (offices) { if (offices.isEmpty) { return Text( 'No offices available.', style: tt.bodySmall?.copyWith( color: cs.onSurfaceVariant, ), ); } final officeNameById = { for (final o in offices) o.id: o.name, }; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ FilledButton.tonalIcon( onPressed: _isLoading ? null : () => _showOfficeSelectionDialog( offices, ), icon: const Icon( Icons.place_outlined, ), label: const Text('Select Offices'), ), const SizedBox(height: 8), AnimatedSize( duration: M3Motion.short, curve: M3Motion.standard_, child: _selectedOfficeIds.isEmpty ? Padding( padding: const EdgeInsets.only( top: 4, ), child: Text( 'No office selected.', style: tt.bodySmall ?.copyWith( color: cs .onSurfaceVariant, ), ), ) : Builder( builder: (context) { final sortedIds = List.from( _selectedOfficeIds, )..sort( ( a, b, ) => (officeNameById[a] ?? a) .toLowerCase() .compareTo( (officeNameById[b] ?? b) .toLowerCase(), ), ); return Wrap( spacing: 8, runSpacing: 8, children: sortedIds.map(( id, ) { final name = officeNameById[id] ?? id; return Chip( label: Text(name), onDeleted: _isLoading ? null : () { setState( () => _selectedOfficeIds .remove( id, ), ); }, ); }).toList(), ); }, ), ), ], ); }, loading: () => const LinearProgressIndicator(), error: (error, _) => Text('Failed to load offices: $error'), ), const SizedBox(height: 24), M3AnimatedSwitcher( child: _isLoading ? const SizedBox( key: ValueKey('loading'), height: 48, child: Center( child: CircularProgressIndicator(), ), ) : FilledButton( key: const ValueKey('sign-up'), onPressed: _handleSignUp, style: FilledButton.styleFrom( minimumSize: const Size.fromHeight( 48, ), ), child: const Text('Create Account'), ), ), ], ), ), ), ), const SizedBox(height: 24), // ── Back to sign in ── TextButton( onPressed: _isLoading ? null : () => context.go('/login'), child: Text.rich( TextSpan( text: 'Already have an account? ', style: tt.bodyMedium?.copyWith( color: cs.onSurfaceVariant, ), children: [ TextSpan( text: 'Sign in', style: TextStyle( color: cs.primary, fontWeight: FontWeight.w600, ), ), ], ), ), ), ], ), ), ), ), ), ), ), ); } Future _showOfficeSelectionDialog(List offices) async { final cs = Theme.of(context).colorScheme; final tempSelected = Set.from(_selectedOfficeIds); await m3ShowDialog( context: context, builder: (dialogCtx) => StatefulBuilder( builder: (ctx2, setStateDialog) { return AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(28), ), title: const Text('Select Offices'), content: Container( width: 480, height: 400, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), color: cs.surfaceContainerLowest, ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: offices.map((office) { final id = office.id; final name = office.name; return CheckboxListTile( value: tempSelected.contains(id), onChanged: (v) { setStateDialog(() { if (v == true) { tempSelected.add(id); } else { tempSelected.remove(id); } }); }, title: Text(name), controlAffinity: ListTileControlAffinity.leading, contentPadding: const EdgeInsets.symmetric(horizontal: 8), ); }).toList(), ), ), ), actions: [ TextButton( onPressed: () => Navigator.of(dialogCtx).pop(), child: const Text('Cancel'), ), FilledButton( onPressed: () { setState(() { _selectedOfficeIds ..clear() ..addAll(tempSelected); }); Navigator.of(dialogCtx).pop(); }, child: const Text('Save'), ), ], ); }, ), ); } void _updatePasswordStrength() { final text = _passwordController.text; var score = 0; if (text.length >= 8) score++; if (text.length >= 12) score++; if (RegExp(r'[A-Z]').hasMatch(text)) score++; if (RegExp(r'[a-z]').hasMatch(text)) score++; if (RegExp(r'\d').hasMatch(text)) score++; if (RegExp(r'[!@#\$%\^&\*(),.?":{}|<>\[\]\\/+=;_-]').hasMatch(text)) { score++; } final normalized = (score / 6).clamp(0.0, 1.0); String label; Color color; if (normalized <= 0.2) { label = 'Very weak'; color = Colors.red; } else if (normalized <= 0.4) { label = 'Weak'; color = Colors.deepOrange; } else if (normalized <= 0.6) { label = 'Fair'; color = Colors.orange; } else if (normalized <= 0.8) { label = 'Strong'; color = Colors.green; } else { label = 'Excellent'; color = Colors.teal; } setState(() { _passwordStrength = normalized; _passwordStrengthLabel = label; _passwordStrengthColor = color; }); } }