diff --git a/creche_app/lib/features/auth/invite_pending_screen.dart b/creche_app/lib/features/auth/invite_pending_screen.dart deleted file mode 100644 index 9f7e153..0000000 --- a/creche_app/lib/features/auth/invite_pending_screen.dart +++ /dev/null @@ -1,309 +0,0 @@ -import 'package:flutter/material.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'; -import '/models/invite.dart'; - -// ─── Cores ─────────────────────────────────────────────── -const _bg = Color(0xFF0D1117); -const _card = Color(0xFF161B22); -const _blue = Color(0xFF4FC3F7); - -String _roleLabel(String r) { - switch (r) { - case 'teacher': return 'Educadora'; - case 'staff': return 'Auxiliar'; - case 'admin': return 'Administrador'; - case 'parent': return 'Encarregado de Educação'; - default: return r; - } -} - -String _roleDesc(String r) { - switch (r) { - case 'teacher': return 'Gerir turmas, registar presenças, escrever diários diários das crianças.'; - case 'staff': return 'Acesso operacional à app da creche com funcionalidades essenciais.'; - case 'admin': return 'Gestão de pagamentos, relatórios e utilizadores do sistema.'; - case 'parent': return 'Consultar o diário do seu filho, ver presenças e comunicar com a equipa.'; - default: return ''; - } -} - -IconData _roleIcon(String r) { - switch (r) { - case 'teacher': return Icons.school_outlined; - case 'staff': return Icons.cleaning_services_outlined; - case 'admin': return Icons.admin_panel_settings_outlined; - case 'parent': return Icons.family_restroom; - default: return Icons.person_outline; - } -} - -Color _roleColor(String r) { - switch (r) { - case 'teacher': return _blue; - case 'staff': return const Color(0xFFA5D6A7); - case 'admin': return const Color(0xFFFF7043); - case 'parent': return const Color(0xFFFFB300); - default: return Colors.grey; - } -} - -/// Verificar convites pendentes quando o utilizador entra pela primeira vez -class InvitePendingScreen extends ConsumerStatefulWidget { - final Invite invite; - const InvitePendingScreen({super.key, required this.invite}); - @override - ConsumerState createState() => _State(); -} - -class _State extends ConsumerState with SingleTickerProviderStateMixin { - late AnimationController _anim; - late Animation _fade; - late Animation _slide; - bool _accepting = false; - bool _rejecting = false; - - @override - void initState() { - super.initState(); - _anim = AnimationController(vsync: this, duration: const Duration(milliseconds: 700)); - _fade = CurvedAnimation(parent: _anim, curve: Curves.easeOut); - _slide = Tween(begin: const Offset(0, 0.06), end: Offset.zero) - .animate(CurvedAnimation(parent: _anim, curve: Curves.easeOut)); - _anim.forward(); - } - - @override - void dispose() { _anim.dispose(); super.dispose(); } - - Future _accept() async { - setState(() => _accepting = true); - final supabase = Supabase.instance.client; - final user = supabase.auth.currentUser!; - - try { - // 1 ─ Actualizar o convite: accepted + expirar imediatamente - await supabase.from('invites').update({ - 'status': 'accepted', - 'accepted_at': DateTime.now().toIso8601String(), - 'expires_at': DateTime.now().toIso8601String(), // expirar agora → nunca mais aparece - }).eq('id', widget.invite.id); - - // 2 ─ Verificar se já tem perfil - final existing = await supabase.from('profiles').select().eq('user_id', user.id).maybeSingle(); - - if (existing == null) { - // 3a ─ Criar o perfil com a função do convite - await supabase.from('profiles').insert({ - 'user_id': user.id, - 'full_name': user.email?.split('@').first ?? 'Utilizador', - 'role': widget.invite.role, - 'phone': widget.invite.phone, - }); - } else { - // 3b ─ Actualizar role do perfil existente - await supabase.from('profiles').update({'role': widget.invite.role}).eq('user_id', user.id); - } - - // 4 ─ Se for encarregado e tiver criança, criar ligação - if (widget.invite.role == 'parent' && widget.invite.childId != null) { - await supabase.from('child_guardians').upsert({ - 'child_id': widget.invite.childId, - 'guardian_id': (await supabase.from('profiles').select('id').eq('user_id', user.id).single())['id'], - 'relationship': 'parent', - }); - } - - // 5 ─ Invalidar o provider e navegar - ref.invalidate(currentProfileProvider); - ref.invalidate(currentSessionProvider); - - if (mounted) { - _showSnack('Bem-vindo à equipa! 🎉', success: true); - await Future.delayed(const Duration(milliseconds: 800)); - if (mounted) context.go('/home'); - } - } catch (e) { - setState(() => _accepting = false); - _showSnack('Erro ao aceitar convite: $e'); - } - } - - Future _reject() async { - setState(() => _rejecting = true); - try { - await Supabase.instance.client.from('invites').update({ - 'status': 'rejected', - 'expires_at': DateTime.now().toIso8601String(), // expirar agora - }).eq('id', widget.invite.id); - await ref.read(authNotifierProvider.notifier).signOut(); - if (mounted) context.go('/login'); - } catch (_) { - setState(() => _rejecting = false); - } - } - - void _showSnack(String msg, {bool success = false}) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(msg, style: const TextStyle(color: Colors.white)), - backgroundColor: success ? const Color(0xFF2ECC71) : Colors.red, - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - )); - } - - @override - Widget build(BuildContext context) { - final inv = widget.invite; - final color = _roleColor(inv.role); - - return Scaffold( - backgroundColor: _bg, - body: SafeArea( - child: FadeTransition( - opacity: _fade, - child: SlideTransition( - position: _slide, - child: SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: Column(children: [ - const SizedBox(height: 32), - // Logo / header - Container( - width: 80, height: 80, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient(colors: [color.withOpacity(0.2), Colors.transparent]), - border: Border.all(color: color.withOpacity(0.4), width: 2), - ), - child: Icon(_roleIcon(inv.role), color: color, size: 36), - ), - const SizedBox(height: 20), - const Text('Convite Recebido 🎉', - style: TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold)), - const SizedBox(height: 8), - Text('A Diretora convidou-te para a equipa', - style: TextStyle(color: Colors.white.withOpacity(0.45), fontSize: 14)), - const SizedBox(height: 32), - - // Card do convite - Container( - width: double.infinity, - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: _card, - borderRadius: BorderRadius.circular(20), - border: Border.all(color: color.withOpacity(0.3), width: 1.5), - boxShadow: [BoxShadow(color: color.withOpacity(0.1), blurRadius: 30, offset: const Offset(0, 8))], - ), - child: Column(children: [ - // Role badge - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: color.withOpacity(0.12), - borderRadius: BorderRadius.circular(30), - border: Border.all(color: color.withOpacity(0.3)), - ), - child: Row(mainAxisSize: MainAxisSize.min, children: [ - Icon(_roleIcon(inv.role), color: color, size: 16), - const SizedBox(width: 8), - Text(_roleLabel(inv.role), - style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 14, letterSpacing: 0.5)), - ]), - ), - const SizedBox(height: 20), - Text(_roleDesc(inv.role), - textAlign: TextAlign.center, - style: TextStyle(color: Colors.white.withOpacity(0.6), fontSize: 14, height: 1.6)), - const SizedBox(height: 20), - const Divider(color: Color(0xFF222244)), - const SizedBox(height: 16), - // Detalhes - _DetailRow(icon: Icons.mail_outline, label: 'Email', value: inv.email), - if (inv.phone != null) ...[ - const SizedBox(height: 8), - _DetailRow(icon: Icons.phone_outlined, label: 'Telefone', value: inv.phone!), - ], - const SizedBox(height: 8), - _DetailRow( - icon: Icons.timer_outlined, - label: 'Expira em', - value: '${inv.expiresAt.difference(DateTime.now()).inDays} dias', - ), - ]), - ), - const SizedBox(height: 28), - - // Botões - _GradientBtn( - label: 'Aceitar e Entrar na Equipa', - color: color, - isLoading: _accepting, - icon: Icons.check_circle_outline, - onTap: _accept, - ), - const SizedBox(height: 12), - GestureDetector( - onTap: _rejecting ? null : _reject, - child: Container( - height: 50, width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - border: Border.all(color: Colors.red.withOpacity(0.4)), - ), - child: Center(child: _rejecting - ? const SizedBox(height: 18, width: 18, child: CircularProgressIndicator(color: Colors.red, strokeWidth: 2)) - : const Text('Recusar convite', style: TextStyle(color: Colors.red, fontSize: 14))), - ), - ), - const SizedBox(height: 32), - ]), - ), - ), - ), - ), - ); - } -} - -class _DetailRow extends StatelessWidget { - final IconData icon; final String label, value; - const _DetailRow({required this.icon, required this.label, required this.value}); - @override - Widget build(BuildContext context) => Row(children: [ - Icon(icon, size: 16, color: const Color(0xFF888888)), - const SizedBox(width: 8), - Text('$label: ', style: const TextStyle(color: Color(0xFF888888), fontSize: 12)), - Expanded(child: Text(value, style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w500), - overflow: TextOverflow.ellipsis)), - ]); -} - -class _GradientBtn extends StatelessWidget { - final String label; final Color color; final bool isLoading; - final IconData icon; final VoidCallback onTap; - const _GradientBtn({required this.label, required this.color, required this.isLoading, required this.icon, required this.onTap}); - @override - Widget build(BuildContext context) => GestureDetector( - onTap: isLoading ? null : onTap, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - height: 54, width: double.infinity, - decoration: BoxDecoration( - gradient: LinearGradient(colors: isLoading ? [color.withOpacity(0.4), color.withOpacity(0.4)] : [color, color.withOpacity(0.7)]), - borderRadius: BorderRadius.circular(14), - boxShadow: isLoading ? [] : [BoxShadow(color: color.withOpacity(0.3), blurRadius: 20, offset: const Offset(0, 6))], - ), - child: Center(child: isLoading - ? const SizedBox(height: 22, width: 22, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2.5)) - : Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(icon, color: Colors.white, size: 20), - const SizedBox(width: 10), - Text(label, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 15)), - ])), - ), - ); -}