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 '../../widgets/responsive_body.dart'; import '../../utils/snackbar.dart'; class SignUpScreen extends ConsumerStatefulWidget { const SignUpScreen({super.key}); @override ConsumerState createState() => _SignUpScreenState(); } class _SignUpScreenState extends ConsumerState { final _formKey = GlobalKey(); final _fullNameController = TextEditingController(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _confirmPasswordController = TextEditingController(); final Set _selectedOfficeIds = {}; double _passwordStrength = 0.0; String _passwordStrengthLabel = 'Very weak'; Color _passwordStrengthColor = Colors.red; bool _isLoading = false; @override void initState() { super.initState(); _passwordController.addListener(_updatePasswordStrength); } @override void 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 officesAsync = ref.watch(officesOnceProvider); return Scaffold( appBar: AppBar(title: const Text('Create Account')), body: ResponsiveBody( maxWidth: 480, padding: const EdgeInsets.symmetric(vertical: 24), child: SingleChildScrollView( child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Center( child: Column( children: [ Image.asset('assets/tasq_ico.png', height: 72, width: 72), const SizedBox(height: 12), Text( 'TasQ', style: Theme.of(context).textTheme.headlineSmall, ), ], ), ), const SizedBox(height: 24), TextFormField( controller: _fullNameController, decoration: const InputDecoration(labelText: 'Full name'), textInputAction: TextInputAction.next, validator: (value) { if (value == null || value.trim().isEmpty) { return 'Full name is required.'; } return null; }, ), const SizedBox(height: 12), TextFormField( controller: _emailController, decoration: const InputDecoration(labelText: 'Email'), keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, validator: (value) { if (value == null || value.trim().isEmpty) { return 'Email is required.'; } return null; }, ), const SizedBox(height: 12), TextFormField( controller: _passwordController, decoration: const InputDecoration(labelText: 'Password'), obscureText: true, 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), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Password strength: $_passwordStrengthLabel', style: Theme.of(context).textTheme.labelMedium, ), const SizedBox(height: 6), LinearProgressIndicator( value: _passwordStrength, minHeight: 8, borderRadius: BorderRadius.circular(8), color: _passwordStrengthColor, backgroundColor: Theme.of( context, ).colorScheme.surfaceContainerHighest, ), ], ), const SizedBox(height: 12), TextFormField( controller: _confirmPasswordController, decoration: const InputDecoration( labelText: 'Confirm password', ), obscureText: true, 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: 12), Text('Offices', style: Theme.of(context).textTheme.titleSmall), const SizedBox(height: 8), officesAsync.when( data: (offices) { if (offices.isEmpty) { return const Text('No offices available.'); } final officeNameById = { for (final o in offices) o.id: o.name, }; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ElevatedButton.icon( onPressed: _isLoading ? null : () => _showOfficeSelectionDialog(offices), icon: const Icon(Icons.place), label: const Text('Select Offices'), ), const SizedBox(height: 8), if (_selectedOfficeIds.isEmpty) const Text('No office selected.') else Wrap( spacing: 8, runSpacing: 8, children: _selectedOfficeIds.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), FilledButton( onPressed: _isLoading ? null : _handleSignUp, child: _isLoading ? const SizedBox( height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Create Account'), ), const SizedBox(height: 12), TextButton( onPressed: _isLoading ? null : () => context.go('/login'), child: const Text('Back to sign in'), ), ], ), ), ), ), ); } Future _showOfficeSelectionDialog(List offices) async { final tempSelected = Set.from(_selectedOfficeIds); await showDialog( context: context, builder: (dialogCtx) => StatefulBuilder( builder: (ctx2, setStateDialog) { return AlertDialog( title: const Text('Select Offices'), content: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 480), 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: EdgeInsets.zero, ); }).toList(), ), ), ), actions: [ TextButton( onPressed: () => Navigator.of(dialogCtx).pop(), child: const Text('Cancel'), ), ElevatedButton( 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; }); } }