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