import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '/core/auth_provider.dart'; class LoginScreen extends ConsumerStatefulWidget { const LoginScreen({super.key}); @override ConsumerState createState() => _LoginScreenState(); } class _LoginScreenState extends ConsumerState with SingleTickerProviderStateMixin { final _emailCtrl = TextEditingController(); final _passCtrl = TextEditingController(); final _nameCtrl = TextEditingController(); // só no registo bool _isRegister = false; // toggle login/registo bool _obscure = true; bool _loading = false; bool _biometricLoading = false; String? _error; late AnimationController _animCtrl; late Animation _fadeAnim; late Animation _slideAnim; @override void initState() { super.initState(); _animCtrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 700)); _fadeAnim = CurvedAnimation(parent: _animCtrl, curve: Curves.easeOut); _slideAnim = Tween(begin: const Offset(0, 0.07), end: Offset.zero) .animate(CurvedAnimation(parent: _animCtrl, curve: Curves.easeOut)); _animCtrl.forward(); } @override void dispose() { _animCtrl.dispose(); _emailCtrl.dispose(); _passCtrl.dispose(); _nameCtrl.dispose(); super.dispose(); } // ── toggle entre Login e Registo ───────────────────────────── void _toggleMode() { setState(() { _isRegister = !_isRegister; _error = null; }); _animCtrl.forward(from: 0); } // ── Login ───────────────────────────────────────────────────── Future _signIn() async { if (_emailCtrl.text.trim().isEmpty || _passCtrl.text.isEmpty) { setState(() => _error = 'Preencha o email e a senha.'); return; } setState(() { _loading = true; _error = null; }); try { await ref.read(authNotifierProvider.notifier).signIn( _emailCtrl.text.trim(), _passCtrl.text, ); if (mounted) context.go('/home'); } on WaitingApprovalException { if (mounted) context.go('/waiting'); } catch (e) { setState(() => _error = _friendly(e.toString())); } finally { if (mounted) setState(() => _loading = false); } } // ── Registo ─────────────────────────────────────────────────── Future _signUp() async { final email = _emailCtrl.text.trim(); final name = _nameCtrl.text.trim(); final pass = _passCtrl.text; if (email.isEmpty || pass.isEmpty || name.isEmpty) { setState(() => _error = 'Preencha todos os campos.'); return; } if (pass.length < 6) { setState(() => _error = 'A senha deve ter pelo menos 6 caracteres.'); return; } setState(() { _loading = true; _error = null; }); try { final sb = Supabase.instance.client; // 1. Criar conta no Supabase Auth final res = await sb.auth.signUp(email: email, password: pass); final user = res.user; if (user == null) throw Exception('Erro ao criar conta.'); // 2. Criar perfil (sem role — será atribuído pelo convite) await sb.from('profiles').upsert({ 'user_id': user.id, 'full_name': name, 'role': 'parent', // role temporário; convite vai actualizar }); // 3. Ir para o splash — ele detecta convites pendentes automaticamente if (mounted) { _showSnack('Conta criada! A verificar convites...', ok: true); await Future.delayed(const Duration(milliseconds: 800)); if (mounted) context.go('/splash'); } } catch (e) { setState(() => _error = _friendly(e.toString())); } finally { if (mounted) setState(() => _loading = false); } } // ── Recuperar senha ─────────────────────────────────────────── Future _forgotPassword() async { final email = _emailCtrl.text.trim(); if (email.isEmpty) { setState(() => _error = 'Digite o seu email primeiro.'); return; } try { await Supabase.instance.client.auth.resetPasswordForEmail(email); if (mounted) _showSnack('Email de recuperação enviado para $email.', ok: true); } catch (e) { setState(() => _error = 'Erro ao enviar email: $e'); } } // ── Biometria ──────────────────────────────────────────────── Future _biometricSignIn() async { if (kIsWeb) { setState(() => _error = 'Biometria não disponível no Web.'); return; } setState(() { _biometricLoading = true; _error = null; }); try { await ref.read(authNotifierProvider.notifier).biometricSignIn(); if (mounted) context.go('/home'); } on WaitingApprovalException { if (mounted) context.go('/waiting'); } catch (e) { setState(() => _error = _friendly(e.toString())); } finally { if (mounted) setState(() => _biometricLoading = false); } } void _showSnack(String msg, {bool ok = false}) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(msg, style: const TextStyle(color: Colors.white)), backgroundColor: ok ? const Color(0xFF2ECC71) : Colors.red, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), duration: const Duration(seconds: 3), )); } String _friendly(String e) { if (e.contains('Invalid login')) return 'Email ou senha incorrectos.'; if (e.contains('Email not confirmed')) return 'Confirme o seu email primeiro.'; if (e.contains('already registered')) return 'Este email já tem conta. Faça login.'; if (e.contains('Password should')) return 'A senha deve ter pelo menos 6 caracteres.'; if (e.contains('IP')) return e; return 'Erro. Tente novamente.'; } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFF0D1117), body: Stack(children: [ Positioned(top: -80, right: -60, child: _Orb(size: 280, color: const Color(0xFF4FC3F7).withOpacity(0.11))), Positioned(bottom: -60, left: -40, child: _Orb(size: 220, color: const Color(0xFFA5D6A7).withOpacity(0.09))), SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 28), child: FadeTransition( opacity: _fadeAnim, child: SlideTransition( position: _slideAnim, child: Column(children: [ const SizedBox(height: 44), // ── Logo ────────────────────────────────────── Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient(colors: [ const Color(0xFF4FC3F7).withOpacity(0.18), Colors.transparent, ]), border: Border.all(color: const Color(0xFF4FC3F7).withOpacity(0.25), width: 1.5), ), child: Image.asset('assets/logo.png', height: 80, errorBuilder: (_, __, ___) => const Icon(Icons.child_care, size: 75, color: Color(0xFF4FC3F7))), ), const SizedBox(height: 20), const Text('Creche e Berçário', style: TextStyle(color: Color(0xFF4FC3F7), fontSize: 13, letterSpacing: 2.5, fontWeight: FontWeight.w500)), const SizedBox(height: 3), const Text('SEMENTES DO FUTURO', style: TextStyle(color: Colors.white, fontSize: 21, fontWeight: FontWeight.w900, letterSpacing: 1.2)), const SizedBox(height: 5), Text('"Conforto, cuidado e aprendizagem"', style: TextStyle(color: Colors.white.withOpacity(0.35), fontSize: 11, fontStyle: FontStyle.italic)), const SizedBox(height: 36), // ── Card principal ──────────────────────────── Container( padding: const EdgeInsets.all(26), decoration: BoxDecoration( color: const Color(0xFF161B22), borderRadius: BorderRadius.circular(24), border: Border.all(color: Colors.white.withOpacity(0.07)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.35), blurRadius: 40, offset: const Offset(0, 16))], ), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ // Título dinâmico Text( _isRegister ? 'Criar conta 📝' : 'Bem-vindo de volta 👋', style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), ), const SizedBox(height: 4), Text( _isRegister ? 'Cria a tua conta — um convite vai atribuir o teu acesso' : 'Faz login para continuar', style: TextStyle(color: Colors.white.withOpacity(0.4), fontSize: 12), ), const SizedBox(height: 22), // Erro if (_error != null) ...[ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: Colors.red.withOpacity(0.1), borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.red.withOpacity(0.3))), child: Row(children: [ const Icon(Icons.error_outline, color: Colors.red, size: 16), const SizedBox(width: 8), Expanded(child: Text(_error!, style: const TextStyle(color: Colors.red, fontSize: 12))), GestureDetector(onTap: () => setState(() => _error = null), child: const Icon(Icons.close, color: Colors.red, size: 14)), ]), ), const SizedBox(height: 16), ], // Nome (só no registo) if (_isRegister) ...[ _Field(ctrl: _nameCtrl, label: 'Nome completo', icon: Icons.person_outline), const SizedBox(height: 14), ], // Email _Field(ctrl: _emailCtrl, label: 'Email', icon: Icons.alternate_email, type: TextInputType.emailAddress), const SizedBox(height: 14), // Senha _Field( ctrl: _passCtrl, label: 'Senha', icon: Icons.lock_outline, obscure: _obscure, onSubmitted: (_) => _isRegister ? _signUp() : _signIn(), suffix: IconButton( icon: Icon(_obscure ? Icons.visibility_off : Icons.visibility, color: Colors.white38, size: 20), onPressed: () => setState(() => _obscure = !_obscure), ), ), if (!_isRegister) ...[ const SizedBox(height: 12), Align( alignment: Alignment.centerRight, child: GestureDetector( onTap: _forgotPassword, child: const Text('Esqueci a senha', style: TextStyle(color: Color(0xFF4FC3F7), fontSize: 12, fontWeight: FontWeight.w500)), ), ), ], const SizedBox(height: 24), // Botão principal _GradButton( label: _isRegister ? 'Criar Conta' : 'Entrar', isLoading: _loading, onTap: _isRegister ? _signUp : _signIn, ), if (!_isRegister) ...[ const SizedBox(height: 14), Row(children: [ Expanded(child: Divider(color: Colors.white.withOpacity(0.09))), Padding(padding: const EdgeInsets.symmetric(horizontal: 12), child: Text('ou', style: TextStyle(color: Colors.white.withOpacity(0.25), fontSize: 11))), Expanded(child: Divider(color: Colors.white.withOpacity(0.09))), ]), const SizedBox(height: 14), _BiometricBtn(isLoading: _biometricLoading, onTap: _biometricSignIn), ], ]), ), const SizedBox(height: 22), // ── Toggle Login / Registo ───────────────────── GestureDetector( onTap: _toggleMode, child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), decoration: BoxDecoration( color: const Color(0xFF161B22), borderRadius: BorderRadius.circular(14), border: Border.all(color: Colors.white.withOpacity(0.07)), ), child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ Text( _isRegister ? 'Já tens conta? ' : 'Ainda não tens conta? ', style: TextStyle(color: Colors.white.withOpacity(0.45), fontSize: 13), ), Text( _isRegister ? 'Fazer login' : 'Criar conta', style: const TextStyle(color: Color(0xFF4FC3F7), fontSize: 13, fontWeight: FontWeight.bold), ), ]), ), ), const SizedBox(height: 20), // Aviso do fluxo de convites if (_isRegister) Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: const Color(0xFF4FC3F7).withOpacity(0.06), borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFF4FC3F7).withOpacity(0.2)), ), child: const Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.info_outline, color: Color(0xFF4FC3F7), size: 16), SizedBox(width: 10), Expanded(child: Text( 'Após criar a conta, se tiveres um convite da Diretora, verás automaticamente o ecrã de convite para aceitar o teu acesso.', style: TextStyle(color: Color(0xFF888888), fontSize: 11, height: 1.5), )), ]), ), const SizedBox(height: 20), Text('v1.0 · Diário do Candengue', style: TextStyle(color: Colors.white.withOpacity(0.15), fontSize: 10)), const SizedBox(height: 24), ]), ), ), ), ), ]), ); } } // ── Widgets auxiliares ───────────────────────────────────────────── class _Field extends StatelessWidget { final TextEditingController ctrl; final String label; final IconData icon; final bool obscure; final TextInputType type; final Widget? suffix; final Function(String)? onSubmitted; const _Field({required this.ctrl, required this.label, required this.icon, this.obscure = false, this.type = TextInputType.text, this.suffix, this.onSubmitted}); @override Widget build(BuildContext context) => TextField( controller: ctrl, obscureText: obscure, keyboardType: type, onSubmitted: onSubmitted, style: const TextStyle(color: Colors.white, fontSize: 14), decoration: InputDecoration( labelText: label, labelStyle: TextStyle(color: Colors.white.withOpacity(0.4), fontSize: 13), prefixIcon: Icon(icon, color: const Color(0xFF4FC3F7).withOpacity(0.7), size: 19), suffixIcon: suffix, filled: true, fillColor: Colors.white.withOpacity(0.05), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.white.withOpacity(0.1))), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFF4FC3F7), width: 1.5)), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15), ), ); } class _GradButton extends StatelessWidget { final String label; final bool isLoading; final VoidCallback onTap; const _GradButton({required this.label, required this.isLoading, required this.onTap}); @override Widget build(BuildContext context) => GestureDetector( onTap: isLoading ? null : onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 200), height: 52, decoration: BoxDecoration( gradient: LinearGradient(colors: isLoading ? [const Color(0xFF2A6A8A), const Color(0xFF2A6A8A)] : [const Color(0xFF4FC3F7), const Color(0xFF0288D1)]), borderRadius: BorderRadius.circular(14), boxShadow: isLoading ? [] : [BoxShadow(color: const Color(0xFF4FC3F7).withOpacity(0.3), blurRadius: 18, offset: const Offset(0, 6))], ), child: Center(child: isLoading ? const SizedBox(height: 22, width: 22, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2.5)) : Text(label, style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold))), ), ); } class _BiometricBtn extends StatelessWidget { final bool isLoading; final VoidCallback onTap; const _BiometricBtn({required this.isLoading, required this.onTap}); @override Widget build(BuildContext context) => GestureDetector( onTap: isLoading ? null : onTap, child: Container( height: 50, decoration: BoxDecoration(color: Colors.white.withOpacity(0.04), borderRadius: BorderRadius.circular(14), border: Border.all(color: Colors.white.withOpacity(0.1))), child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ isLoading ? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Color(0xFF4FC3F7), strokeWidth: 2)) : const Icon(Icons.fingerprint, color: Color(0xFF4FC3F7), size: 22), const SizedBox(width: 10), Text(kIsWeb ? 'Biometria (só mobile)' : 'Entrar com biometria', style: TextStyle(color: kIsWeb ? Colors.white24 : Colors.white60, fontSize: 13)), ]), ), ); } class _Orb extends StatelessWidget { final double size; final Color color; const _Orb({required this.size, required this.color}); @override Widget build(BuildContext context) => Container( width: size, height: size, decoration: BoxDecoration(shape: BoxShape.circle, color: color, boxShadow: [BoxShadow(color: color, blurRadius: size / 2)]), ); }