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)), ])), ), ); }