310 lines
12 KiB
Dart
310 lines
12 KiB
Dart
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)),
|
|
])),
|
|
),
|
|
);
|
|
}
|