import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:uuid/uuid.dart'; import '/core/auth_provider.dart'; import '/models/profile.dart'; import '/models/daily_access_approval.dart'; import '/models/invite.dart'; // ── Paleta ──────────────────────────────────────────────────────────── const _bg = Color(0xFF0D1117); const _card = Color(0xFF161B22); const _blue = Color(0xFF4FC3F7); const _green = Color(0xFF2ECC71); const _amber = Color(0xFFFFB300); const _red = Color(0xFFE74C3C); // ── Role helpers ────────────────────────────────────────────────────── Color _rc(String r) { switch (r) { case 'principal': return const Color(0xFFFFD700); case 'admin': return const Color(0xFFFF7043); case 'teacher': return _blue; case 'staff': return const Color(0xFFA5D6A7); case 'parent': return _amber; default: return const Color(0xFF666688); } } String _rl(String r) { switch (r) { case 'principal': return 'Diretora'; case 'admin': return 'Admin'; case 'teacher': return 'Educadora'; case 'staff': return 'Auxiliar'; case 'parent': return 'Encarregado'; default: return r; } } const _rolesMeta = [ {'value': 'teacher', 'label': '👩‍🏫 Educadora', 'desc': 'Turmas, diários, presenças'}, {'value': 'staff', 'label': '🧹 Auxiliar', 'desc': 'Acesso operacional básico'}, {'value': 'admin', 'label': '⚙️ Administrador', 'desc': 'Pagamentos e relatórios'}, {'value': 'parent', 'label': '👨‍👧 Encarregado', 'desc': 'Só diário do(s) filho(s)'}, ]; // ═════════════════════════════════════════════════════════════════════ // SCREEN // ═════════════════════════════════════════════════════════════════════ class UsersManagementScreen extends ConsumerStatefulWidget { const UsersManagementScreen({super.key}); @override ConsumerState createState() => _ScreenState(); } class _ScreenState extends ConsumerState with SingleTickerProviderStateMixin { late TabController _tab; @override void initState() { super.initState(); _tab = TabController(length: 4, vsync: this); } @override void dispose() { _tab.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final myRole = ref.watch(currentProfileProvider).valueOrNull?.role ?? ''; final canManage = myRole == 'principal' || myRole == 'admin'; return Scaffold( backgroundColor: _bg, appBar: AppBar( backgroundColor: _card, elevation: 0, title: const Text('Utilizadores', style: TextStyle(color: _blue, fontWeight: FontWeight.bold, fontSize: 18)), bottom: TabBar( controller: _tab, indicatorColor: _blue, indicatorWeight: 3, labelColor: _blue, unselectedLabelColor: const Color(0xFF555577), labelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 11), isScrollable: true, tabAlignment: TabAlignment.start, tabs: const [ Tab(icon: Icon(Icons.people, size: 17), text: 'Equipa'), Tab(icon: Icon(Icons.schedule, size: 17), text: 'Acessos'), Tab(icon: Icon(Icons.mail_outline, size: 17), text: 'Convites'), Tab(icon: Icon(Icons.person_add, size: 17), text: 'Convidar'), ], ), ), body: TabBarView( controller: _tab, children: [ _TeamTab(canManage: canManage), const _AccessesTab(), _InvitesTab(canManage: canManage), _SendInviteTab(onSent: () => _tab.animateTo(2)), ], ), ); } } // ═════════════════════════════════════════════════════════════════════ // TAB 1 — EQUIPA // ═════════════════════════════════════════════════════════════════════ class _TeamTab extends ConsumerWidget { final bool canManage; const _TeamTab({required this.canManage}); @override Widget build(BuildContext context, WidgetRef ref) { final sb = Supabase.instance.client; final myUid = sb.auth.currentUser?.id; return StreamBuilder>>( stream: sb.from('profiles').stream(primaryKey: ['id']), builder: (ctx, snap) { if (!snap.hasData) return const _Loader(); final order = ['principal','admin','teacher','staff','parent']; final list = snap.data!.map(Profile.fromMap).toList() ..sort((a,b) => order.indexOf(a.role).compareTo(order.indexOf(b.role))); final groups = >{}; for (final p in list) (groups[p.role] ??= []).add(p); return ListView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 100), children: [ _StatsRow(profiles: list), const SizedBox(height: 20), for (final role in order) if (groups[role] != null) ...[ _SecHeader(label: _rl(role), color: _rc(role), count: groups[role]!.length), ...groups[role]!.map((p) => _UserTile( profile: p, isSelf: p.userId == myUid, canDelete: canManage && p.role != 'principal' && p.userId != myUid, canChangeRole: canManage && p.role != 'principal', onDelete: () => _deleteDialog(ctx, ref, p, sb), onChangeRole: () => _roleDialog(ctx, p, sb), )), const SizedBox(height: 6), ], ], ); }, ); } // ── Diálogo apagar ─────────────────────────────────────────────── void _deleteDialog(BuildContext ctx, WidgetRef ref, Profile p, SupabaseClient sb) { showDialog( context: ctx, builder: (_) => AlertDialog( backgroundColor: _card, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), title: const Row(children: [ Icon(Icons.warning_amber_rounded, color: _red, size: 24), SizedBox(width: 10), Text('Remover utilizador', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)), ]), content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ RichText(text: TextSpan(style: const TextStyle(color: Color(0xFFBBBBBB), fontSize: 14, height: 1.6), children: [ const TextSpan(text: 'Vais remover '), TextSpan(text: p.fullName, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), const TextSpan(text: ' ('), TextSpan(text: _rl(p.role), style: TextStyle(color: _rc(p.role))), const TextSpan(text: ') do sistema.\nEsta acção é irreversível.'), ])), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration(color: _red.withOpacity(0.07), borderRadius: BorderRadius.circular(10), border: Border.all(color: _red.withOpacity(0.3))), child: const Text('⚠️ O perfil será apagado. Para apagar também a conta de autenticação vai ao painel Supabase → Authentication → Users.', style: TextStyle(color: _red, fontSize: 11, height: 1.5)), ), ]), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancelar', style: TextStyle(color: Color(0xFF888888)))), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: _red, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))), onPressed: () async { Navigator.pop(ctx); try { // 1. Remove child_guardians links try { await sb.from('child_guardians').delete().eq('guardian_id', p.id); } catch (_) {} // 2. Nullify invited_by on invites (can't delete — RLS only allows admin by id) try { await sb.from('invites').update({'invited_by': null}).eq('invited_by', p.id); } catch (_) {} // 3. Remove daily access requests try { await sb.from('daily_access_approvals').delete().eq('user_id', p.id); } catch (_) {} // 4. Remove the profile (this triggers cascade) await sb.from('profiles').delete().eq('id', p.id); // O utilizador perde o acesso imediatamente: // sem perfil → authorize() retorna false → todas as queries falham → é redirecionado para login if (ctx.mounted) _snack(ctx, '${p.fullName} removido. O acesso foi revogado.', ok: true); } catch (e) { if (ctx.mounted) _snack(ctx, 'Erro: ${e.toString().replaceAll('Exception: ', '')}'); } }, child: const Text('Remover', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), ), ], ), ); } // ── Diálogo alterar role ───────────────────────────────────────── void _roleDialog(BuildContext ctx, Profile p, SupabaseClient sb) { // Diretora não pode ter o role alterado por ninguém (excepto no SQL) if (p.role == 'principal') { ScaffoldMessenger.of(ctx).showSnackBar(const SnackBar( content: Text('A função da Diretora não pode ser alterada aqui.'), backgroundColor: Color(0xFFE74C3C), behavior: SnackBarBehavior.floating)); return; } // Admin não pode alterar o próprio role final currentUser = sb.auth.currentUser; if (currentUser != null && p.userId == currentUser.id) { ScaffoldMessenger.of(ctx).showSnackBar(const SnackBar( content: Text('Não podes alterar a tua própria função.'), backgroundColor: Color(0xFFE74C3C), behavior: SnackBarBehavior.floating)); return; } String picked = p.role; showDialog( context: ctx, builder: (_) => StatefulBuilder( builder: (_, set) => AlertDialog( backgroundColor: _card, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), title: Text('Função de ${p.fullName.split(' ').first}', style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.bold)), content: Column(mainAxisSize: MainAxisSize.min, children: _rolesMeta.map((r) { final sel = picked == r['value']; final c = _rc(r['value']!); return GestureDetector( onTap: () => set(() => picked = r['value']!), child: AnimatedContainer( duration: const Duration(milliseconds: 120), margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: sel ? c.withOpacity(0.1) : Colors.transparent, borderRadius: BorderRadius.circular(12), border: Border.all(color: sel ? c : Colors.white.withOpacity(0.08)), ), child: Row(children: [ Expanded(child: Text(r['label']!, style: TextStyle(color: sel ? c : Colors.white, fontWeight: FontWeight.w600, fontSize: 13))), if (sel) Icon(Icons.check_circle, color: c, size: 18), ]), ), ); }).toList(), ), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancelar', style: TextStyle(color: Color(0xFF888888)))), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: _blue, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))), onPressed: () async { Navigator.pop(ctx); if (picked == p.role) return; await sb.from('profiles').update({'role': picked}).eq('id', p.id); if (ctx.mounted) _snack(ctx, 'Função actualizada para ${_rl(picked)}.', ok: true); }, child: const Text('Guardar', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), ), ], ), ), ); } } class _StatsRow extends StatelessWidget { final List profiles; const _StatsRow({required this.profiles}); @override Widget build(BuildContext context) { final c = {}; for (final p in profiles) c[p.role] = (c[p.role] ?? 0) + 1; return Row(children: [ for (final t in [('principal','👑','Dir.'),('teacher','👩‍🏫','Educ.'),('staff','🧹','Aux.'),('parent','👨‍👧','Enc.')]) Expanded(child: Container( margin: const EdgeInsets.only(right: 8), padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration(color: _card, borderRadius: BorderRadius.circular(12), border: Border.all(color: _rc(t.$1).withOpacity(0.2))), child: Column(children: [ Text(t.$2, style: const TextStyle(fontSize: 16)), Text('${c[t.$1] ?? 0}', style: TextStyle(color: _rc(t.$1), fontWeight: FontWeight.bold, fontSize: 16)), Text(t.$3, style: const TextStyle(color: Color(0xFF666688), fontSize: 9)), ]), )), ]); } } class _SecHeader extends StatelessWidget { final String label; final Color color; final int count; const _SecHeader({required this.label, required this.color, required this.count}); @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.only(bottom: 8, top: 4), child: Row(children: [ Container(width: 3, height: 16, decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(2))), const SizedBox(width: 8), Text(label.toUpperCase(), style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 11, letterSpacing: 1)), const SizedBox(width: 8), Container(padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), decoration: BoxDecoration(color: color.withOpacity(0.12), borderRadius: BorderRadius.circular(10)), child: Text('$count', style: TextStyle(color: color, fontSize: 10, fontWeight: FontWeight.bold))), ]), ); } class _UserTile extends StatelessWidget { final Profile profile; final bool isSelf, canDelete, canChangeRole; final VoidCallback onDelete, onChangeRole; const _UserTile({required this.profile, required this.isSelf, required this.canDelete, required this.canChangeRole, required this.onDelete, required this.onChangeRole}); @override Widget build(BuildContext context) { final c = _rc(profile.role); return Container( margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration(color: _card, borderRadius: BorderRadius.circular(14), border: Border.all(color: isSelf ? _blue.withOpacity(0.5) : c.withOpacity(0.12))), child: Padding( padding: const EdgeInsets.all(12), child: Row(children: [ // Avatar Stack(children: [ CircleAvatar(radius: 24, backgroundColor: c.withOpacity(0.12), backgroundImage: profile.avatarUrl != null ? NetworkImage(profile.avatarUrl!) : null, child: profile.avatarUrl == null ? Text(profile.fullName.isNotEmpty ? profile.fullName[0].toUpperCase() : '?', style: TextStyle(color: c, fontWeight: FontWeight.bold, fontSize: 18)) : null), if (isSelf) Positioned(bottom: 0, right: 0, child: Container(width: 11, height: 11, decoration: BoxDecoration(color: _green, shape: BoxShape.circle, border: Border.all(color: _card, width: 2)))), ]), const SizedBox(width: 12), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Expanded(child: Text(profile.fullName, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14), overflow: TextOverflow.ellipsis)), if (isSelf) Container(margin: const EdgeInsets.only(left: 5), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration(color: _blue.withOpacity(0.15), borderRadius: BorderRadius.circular(8)), child: const Text('eu', style: TextStyle(color: _blue, fontSize: 10, fontWeight: FontWeight.bold))), ]), Text(profile.phone ?? 'Sem telefone', style: const TextStyle(color: Color(0xFF666688), fontSize: 12)), ])), const SizedBox(width: 8), Column(crossAxisAlignment: CrossAxisAlignment.end, children: [ // Badge role (clicável para mudar) GestureDetector( onTap: canChangeRole ? onChangeRole : null, child: Container( padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4), decoration: BoxDecoration(color: c.withOpacity(0.12), borderRadius: BorderRadius.circular(20), border: canChangeRole ? Border.all(color: c.withOpacity(0.35)) : null), child: Row(mainAxisSize: MainAxisSize.min, children: [ Text(_rl(profile.role), style: TextStyle(color: c, fontSize: 11, fontWeight: FontWeight.bold)), if (canChangeRole) ...[const SizedBox(width: 4), Icon(Icons.edit, size: 10, color: c.withOpacity(0.6))], ]), ), ), if (canDelete) ...[ const SizedBox(height: 6), GestureDetector( onTap: onDelete, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration(color: _red.withOpacity(0.1), borderRadius: BorderRadius.circular(8), border: Border.all(color: _red.withOpacity(0.35))), child: const Row(mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.person_remove, size: 11, color: _red), SizedBox(width: 3), Text('Remover', style: TextStyle(color: _red, fontSize: 10, fontWeight: FontWeight.bold)), ]), ), ), ], ]), ]), ), ); } } // ═════════════════════════════════════════════════════════════════════ // TAB 2 — ACESSOS HOJE // ═════════════════════════════════════════════════════════════════════ class _AccessesTab extends ConsumerWidget { const _AccessesTab(); @override Widget build(BuildContext context, WidgetRef ref) { final sb = Supabase.instance.client; final today = DateTime.now().toIso8601String().split('T')[0]; return StreamBuilder>>( stream: sb.from('daily_access_approvals').stream(primaryKey: ['id']) .map((r) => r.where((x) => x['approval_date'] == today).toList()), builder: (_, snap) { if (!snap.hasData) return const _Loader(); if (snap.data!.isEmpty) return _empty('Sem pedidos de acesso hoje', Icons.access_time_outlined); final list = snap.data!.map(DailyAccessApproval.fromMap).toList() ..sort((a,b) => a.status.compareTo(b.status)); return ListView(padding: const EdgeInsets.all(16), children: list.map((a) => _ApprovalTile(a: a, sb: sb)).toList()); }, ); } } class _ApprovalTile extends StatelessWidget { final DailyAccessApproval a; final SupabaseClient sb; const _ApprovalTile({required this.a, required this.sb}); Color get _c => a.status == 'approved' ? _green : a.status == 'rejected' ? _red : _amber; @override Widget build(BuildContext context) => Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.all(14), decoration: BoxDecoration(color: _card, borderRadius: BorderRadius.circular(14), border: Border.all(color: _c.withOpacity(0.25))), child: Column(children: [ Row(children: [ CircleAvatar(radius: 18, backgroundColor: _c.withOpacity(0.12), child: Icon(Icons.person_outline, color: _c, size: 18)), const SizedBox(width: 12), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('ID: ${a.userId.length > 12 ? a.userId.substring(0,12) : a.userId}...', style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w500)), if (a.ipAddress != null) Text('IP: ${a.ipAddress}', style: const TextStyle(color: Color(0xFF888888), fontSize: 11)), ])), _Badge(text: a.status.toUpperCase(), color: _c), ]), if (a.status == 'pending') ...[ const SizedBox(height: 12), const Divider(color: Color(0xFF1E2233), height: 1), const SizedBox(height: 12), Row(children: [ Expanded(child: _Btn(label: '✓ Aprovar', color: _green, onTap: () => sb.from('daily_access_approvals').update({'status':'approved','approved_at':DateTime.now().toIso8601String(),'approved_by':sb.auth.currentUser?.id}).eq('id',a.id))), const SizedBox(width: 8), Expanded(child: _Btn(label: '✕ Rejeitar', color: _red, onTap: () => sb.from('daily_access_approvals').update({'status':'rejected'}).eq('id',a.id))), ]), ], ]), ); } // ═════════════════════════════════════════════════════════════════════ // TAB 3 — LISTA CONVITES // ═════════════════════════════════════════════════════════════════════ class _InvitesTab extends ConsumerWidget { final bool canManage; const _InvitesTab({required this.canManage}); @override Widget build(BuildContext context, WidgetRef ref) { final sb = Supabase.instance.client; return StreamBuilder>>( stream: sb.from('invites').stream(primaryKey: ['id']), builder: (_, snap) { if (snap.hasError) return _errBox('Tabela "invites" não existe.\nCorre o COMPLETE_MIGRATION.sql.'); if (!snap.hasData) return const _Loader(); if (snap.data!.isEmpty) return _empty('Nenhum convite enviado ainda', Icons.mail_outline); final list = snap.data!.map(Invite.fromMap).toList() ..sort((a,b) => b.createdAt.compareTo(a.createdAt)); return ListView(padding: const EdgeInsets.all(16), children: list.map((i) => _InviteTile(inv: i, sb: sb, canManage: canManage)).toList()); }, ); } } class _InviteTile extends StatelessWidget { final Invite inv; final SupabaseClient sb; final bool canManage; const _InviteTile({required this.inv, required this.sb, required this.canManage}); String get _sl { if (inv.isExpired && inv.status=='pending') return 'EXPIRADO'; switch(inv.status){case 'accepted':return 'ACEITE';case 'rejected':return 'RECUSADO';default:return 'PENDENTE';} } Color get _c { if (inv.isExpired) return Colors.grey; switch(inv.status){case 'accepted':return _green;case 'rejected':return _red;default:return _amber;} } String _fmt(DateTime d) => '${d.day.toString().padLeft(2,'0')}/${d.month.toString().padLeft(2,'0')} ${d.hour.toString().padLeft(2,'0')}:${d.minute.toString().padLeft(2,'0')}'; @override Widget build(BuildContext context) => Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.all(14), decoration: BoxDecoration(color: _card, borderRadius: BorderRadius.circular(14), border: Border.all(color: _c.withOpacity(0.2))), child: Row(children: [ CircleAvatar(radius: 20, backgroundColor: _rc(inv.role).withOpacity(0.12), child: Text(inv.email[0].toUpperCase(), style: TextStyle(color: _rc(inv.role), fontWeight: FontWeight.bold))), const SizedBox(width: 12), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(inv.email, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13), overflow: TextOverflow.ellipsis), Text(_rl(inv.role), style: TextStyle(color: _rc(inv.role), fontSize: 11)), Text('Exp: ${_fmt(inv.expiresAt)}', style: const TextStyle(color: Color(0xFF666688), fontSize: 10)), ])), const SizedBox(width: 8), Column(crossAxisAlignment: CrossAxisAlignment.end, children: [ _Badge(text: _sl, color: _c), if (canManage && inv.status == 'pending' && !inv.isExpired) ...[ const SizedBox(height: 6), GestureDetector(onTap: () => sb.from('invites').delete().eq('id', inv.id), child: const Text('Cancelar', style: TextStyle(color: _red, fontSize: 11))), ], ]), ]), ); } // ═════════════════════════════════════════════════════════════════════ // TAB 4 — ENVIAR CONVITE // ═════════════════════════════════════════════════════════════════════ class _SendInviteTab extends ConsumerStatefulWidget { final VoidCallback onSent; const _SendInviteTab({required this.onSent}); @override ConsumerState<_SendInviteTab> createState() => _SendState(); } class _SendState extends ConsumerState<_SendInviteTab> { final _eCtrl = TextEditingController(); final _pCtrl = TextEditingController(); final _nCtrl = TextEditingController(); String _role = 'teacher'; bool _loading = false; String? _err, _ok; String? _childId; List> _children = []; @override void initState() { super.initState(); _fetchChildren(); } @override void dispose() { _eCtrl.dispose(); _pCtrl.dispose(); _nCtrl.dispose(); super.dispose(); } Future _fetchChildren() async { try { final d = await Supabase.instance.client.from('children').select('id,first_name,last_name').order('first_name'); if (mounted) setState(() => _children = List>.from(d)); } catch (_) {} } Future _send() async { final email = _eCtrl.text.trim(); final name = _nCtrl.text.trim(); if (email.isEmpty || name.isEmpty) { setState(() => _err = 'Preencha o nome e o email.'); return; } if (!RegExp(r'^[\w\-\.]+@[\w\-\.]+\.\w{2,}$').hasMatch(email)) { setState(() => _err = 'Email inválido.'); return; } if (_role == 'parent' && _childId == null) { setState(() => _err = 'Seleccione a criança.'); return; } setState(() { _loading = true; _err = null; _ok = null; }); try { final sb = Supabase.instance.client; final me = await sb.from('profiles').select('id').eq('user_id', sb.auth.currentUser!.id).single(); final inviteId = const Uuid().v4(); // 1. Guardar na BD primeiro await sb.from('invites').insert({ 'id': inviteId, 'email': email, 'role': _role, 'phone': _pCtrl.text.trim().isEmpty ? null : _pCtrl.text.trim(), 'invited_by': me['id'], 'status': 'pending', 'expires_at': DateTime.now().add(const Duration(days: 7)).toIso8601String(), 'child_id': _childId, }); // 2. Chamar Edge Function para enviar email String emailStatus = ''; try { final accessToken = sb.auth.currentSession?.accessToken ?? ''; final res = await sb.functions.invoke('send-invite', headers: {'Authorization': 'Bearer $accessToken'}, body: { 'email': email, 'role': _role, 'name': name, 'phone': _pCtrl.text.trim().isEmpty ? null : _pCtrl.text.trim(), 'childId': _childId, 'inviteId': inviteId, }); final data = res.data as Map?; if (data?['userExists'] == true) { emailStatus = '\nO utilizador já tem conta — o convite aparece ao fazer login.'; } else { emailStatus = '\n📧 Email enviado com link para criar conta!'; } } catch (emailErr) { // Email falhou mas o convite está na BD — não é crítico emailStatus = '\n⚠️ Email automático não disponível. Partilha o link da app manualmente.'; } if (mounted) { setState(() { _ok = '✅ Convite criado para $email!$emailStatus'; _eCtrl.clear(); _nCtrl.clear(); _pCtrl.clear(); _childId = null; }); widget.onSent(); } } catch (e) { setState(() => _err = 'Erro: $e'); } finally { if (mounted) setState(() => _loading = false); } } @override Widget build(BuildContext context) { return SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: LinearGradient(colors: [_blue.withOpacity(0.1), Colors.transparent]), borderRadius: BorderRadius.circular(16), border: Border.all(color: _blue.withOpacity(0.2)), ), child: const Row(children: [ Icon(Icons.verified_user_outlined, color: _blue, size: 28), SizedBox(width: 14), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Sistema de Convites', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 15)), SizedBox(height: 3), Text('A pessoa aceita o convite ao entrar na app. Válido 7 dias.', style: TextStyle(color: Color(0xFF888888), fontSize: 12)), ])), ]), ), const SizedBox(height: 20), if (_err != null) _fb(_err!, false), if (_ok != null) _fb(_ok!, true), _lbl('Nome completo'), _tf(_nCtrl, 'Ex: Maria da Silva', Icons.person_outline), const SizedBox(height: 14), _lbl('Email'), _tf(_eCtrl, 'email@exemplo.com', Icons.alternate_email, TextInputType.emailAddress), const SizedBox(height: 14), _lbl('Telefone (opcional)'), _tf(_pCtrl, '+244 9xx xxx xxx', Icons.phone_outlined, TextInputType.phone), const SizedBox(height: 14), _lbl('Função'), ..._rolesMeta.map((r) { final sel = _role == r['value']; final c = _rc(r['value']!); return GestureDetector( onTap: () => setState(() { _role = r['value']!; _childId = null; }), child: AnimatedContainer( duration: const Duration(milliseconds: 120), margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( color: sel ? c.withOpacity(0.1) : _card, borderRadius: BorderRadius.circular(12), border: Border.all(color: sel ? c : Colors.white.withOpacity(0.08), width: sel ? 1.5 : 1), ), child: Row(children: [ Text(r['label']!, style: TextStyle(color: sel ? c : Colors.white, fontWeight: FontWeight.w600, fontSize: 14)), const SizedBox(width: 6), Expanded(child: Text(r['desc']!, style: const TextStyle(color: Color(0xFF666688), fontSize: 11))), AnimatedContainer(duration: const Duration(milliseconds: 120), width: 20, height: 20, decoration: BoxDecoration(shape: BoxShape.circle, color: sel ? c : Colors.transparent, border: Border.all(color: sel ? c : Colors.white24, width: 2)), child: sel ? const Icon(Icons.check, size: 12, color: Colors.white) : null), ]), ), ); }), if (_role == 'parent') ...[ const SizedBox(height: 14), _lbl('Criança associada *'), Container( decoration: BoxDecoration(color: _card, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white.withOpacity(0.1))), child: DropdownButtonHideUnderline( child: DropdownButton( value: _childId, isExpanded: true, dropdownColor: _card, style: const TextStyle(color: Colors.white, fontSize: 14), padding: const EdgeInsets.symmetric(horizontal: 14), hint: const Text('Seleccione a criança', style: TextStyle(color: Color(0xFF888888))), items: _children.map((c) => DropdownMenuItem(value: c['id'] as String, child: Text('${c['first_name']} ${c['last_name']}'))).toList(), onChanged: (v) => setState(() => _childId = v), ), ), ), const SizedBox(height: 8), Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration(color: _blue.withOpacity(0.06), borderRadius: BorderRadius.circular(10), border: Border.all(color: _blue.withOpacity(0.15))), child: const Row(children: [ Icon(Icons.info_outline, color: _blue, size: 14), SizedBox(width: 8), Expanded(child: Text('O encarregado só acede ao diário, presenças e mensagens do seu filho.', style: TextStyle(color: Color(0xFF888888), fontSize: 11))), ]), ), ], const SizedBox(height: 24), GestureDetector( onTap: _loading ? null : _send, child: AnimatedContainer( duration: const Duration(milliseconds: 200), height: 54, width: double.infinity, decoration: BoxDecoration( gradient: LinearGradient(colors: _loading ? [const Color(0xFF1A3A4A), const Color(0xFF1A3A4A)] : [_blue, const Color(0xFF0288D1)]), borderRadius: BorderRadius.circular(14), boxShadow: _loading ? [] : [BoxShadow(color: _blue.withOpacity(0.3), blurRadius: 20, offset: const Offset(0,6))], ), child: Center(child: _loading ? const SizedBox(height: 22, width: 22, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2.5)) : const Row(mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.send_outlined, color: Colors.white, size: 18), SizedBox(width: 8), Text('Enviar Convite', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16)), ])), ), ), const SizedBox(height: 24), // Como funciona Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: _card, borderRadius: BorderRadius.circular(14), border: Border.all(color: Colors.white.withOpacity(0.05))), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row(children: [Icon(Icons.help_outline, color: _blue, size: 15), SizedBox(width: 8), Text('Como funciona?', style: TextStyle(color: _blue, fontWeight: FontWeight.bold, fontSize: 13))]), const SizedBox(height: 12), ...[(_blue,'1','Envias o convite (email + função)'), (_amber,'2','A pessoa instala a app e cria conta'), (_green,'3','Ao entrar, vê automaticamente o convite'), (_blue,'4','Aceita → fica com o role atribuído'), (_green,'5','Acede às suas funcionalidades')].map((s) => Padding( padding: const EdgeInsets.only(bottom: 7), child: Row(children: [ Container(width: 22, height: 22, decoration: BoxDecoration(color: s.$1.withOpacity(0.15), shape: BoxShape.circle), child: Center(child: Text(s.$2, style: TextStyle(color: s.$1, fontSize: 11, fontWeight: FontWeight.bold)))), const SizedBox(width: 10), Expanded(child: Text(s.$3, style: const TextStyle(color: Color(0xFFAAAAAA), fontSize: 12))), ]), )), ]), ), const SizedBox(height: 40), ]), ); } Widget _lbl(String t) => Padding(padding: const EdgeInsets.only(bottom: 7), child: Text(t, style: const TextStyle(color: Color(0xFFAAAAAA), fontSize: 12, fontWeight: FontWeight.w600, letterSpacing: 0.5))); Widget _tf(TextEditingController c, String hint, IconData icon, [TextInputType? t]) => TextField( controller: c, keyboardType: t, style: const TextStyle(color: Colors.white, fontSize: 14), decoration: InputDecoration( hintText: hint, hintStyle: const TextStyle(color: Color(0xFF555577), fontSize: 13), prefixIcon: Icon(icon, color: _blue.withOpacity(0.7), size: 20), filled: true, fillColor: _card, enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.white.withOpacity(0.1))), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: _blue, width: 1.5)), contentPadding: const EdgeInsets.symmetric(vertical: 14), ), ); Widget _fb(String msg, bool ok) => Container( margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: (ok ? _green : _red).withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: (ok ? _green : _red).withOpacity(0.4))), child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(ok ? Icons.check_circle_outline : Icons.error_outline, color: ok ? _green : _red, size: 18), const SizedBox(width: 10), Expanded(child: Text(msg, style: TextStyle(color: ok ? _green : _red, fontSize: 12))), GestureDetector(onTap: () => setState(() => ok ? _ok = null : _err = null), child: const Icon(Icons.close, color: Colors.white30, size: 16)), ]), ); } // ── Widgets comuns ───────────────────────────────────────────────────── class _Badge extends StatelessWidget { final String text; final Color color; const _Badge({required this.text, required this.color}); @override Widget build(BuildContext context) => Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration(color: color.withOpacity(0.12), borderRadius: BorderRadius.circular(20)), child: Text(text, style: TextStyle(color: color, fontSize: 10, fontWeight: FontWeight.bold)), ); } class _Btn extends StatelessWidget { final String label; final Color color; final VoidCallback onTap; const _Btn({required this.label, required this.color, required this.onTap}); @override Widget build(BuildContext context) => GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(10), border: Border.all(color: color.withOpacity(0.4))), child: Center(child: Text(label, style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 13))), ), ); } class _Loader extends StatelessWidget { const _Loader(); @override Widget build(BuildContext context) => const Center(child: CircularProgressIndicator(color: _blue)); } Widget _empty(String msg, IconData icon) => Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 64, color: Colors.white.withOpacity(0.07)), const SizedBox(height: 12), Text(msg, style: const TextStyle(color: Color(0xFF888888))), ])); Widget _errBox(String msg) => Center(child: Container( margin: const EdgeInsets.all(24), padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: _red.withOpacity(0.07), borderRadius: BorderRadius.circular(14), border: Border.all(color: _red.withOpacity(0.3))), child: Column(mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.error_outline, color: _red, size: 32), const SizedBox(height: 10), Text(msg, style: const TextStyle(color: _red, fontSize: 13), textAlign: TextAlign.center), const SizedBox(height: 8), const Text('Corre o COMPLETE_MIGRATION.sql no Supabase SQL Editor.', style: TextStyle(color: Color(0xFF888888), fontSize: 11), textAlign: TextAlign.center), ]), )); void _snack(BuildContext ctx, String msg, {bool ok = false}) => ScaffoldMessenger.of(ctx).showSnackBar(SnackBar( content: Text(msg, style: const TextStyle(color: Colors.white)), backgroundColor: ok ? _green : _red, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ));