Eliminar creche_app/lib/features/auth/invite_pending_screen.dart
This commit is contained in:
parent
4de3bc12fd
commit
8e10d73296
|
|
@ -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<InvitePendingScreen> createState() => _State();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _State extends ConsumerState<InvitePendingScreen> with SingleTickerProviderStateMixin {
|
|
||||||
late AnimationController _anim;
|
|
||||||
late Animation<double> _fade;
|
|
||||||
late Animation<Offset> _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<Offset>(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<void> _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<void> _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)),
|
|
||||||
])),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue