import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; import 'package:lottie/lottie.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'dart:async'; import 'dart:math' as math; import 'dart:io'; import 'package:shared_preferences/shared_preferences.dart'; import 'main.dart' show supabase; import 'quiz/quiz1.dart'; import 'quiz/quiz_prefs.dart'; import 'screens/settings_screen.dart'; import 'screens/video_screen.dart'; import 'widgets/animated_nav_icon.dart'; import 'widgets/app_dialogs.dart'; import 'widgets/app_gradients.dart'; import 'widgets/entrance.dart'; import 'widgets/tap_bounce.dart'; /// Nomes só podem ter letras (incluindo acentuadas) e espaços — sem números. final RegExp _namePattern = RegExp(r"^[a-zA-ZÀ-ÖØ-öø-ÿ' -]+$"); class LoggedHomeScreen extends StatefulWidget { const LoggedHomeScreen({super.key}); @override State createState() => _LoggedHomeScreenState(); } class _LoggedHomeScreenState extends State with SingleTickerProviderStateMixin { static const Color _teal = Color(0xFF2F9E94); static const String _kPendingQuizScopeKey = 'pending_quiz_scope_v1'; static const double _collapsedAppBarHeight = 80; static const double _expandedAppBarHeight = 190; static const double _nameOnlyAppBarHeight = 160; int _index = 0; int _selectedChildIndex = 0; String? _selectedChildName; String? _selectedChildScopeId; int? _lastScore; int? _lastMaxScore; String _cachedUserName = 'Sem nome'; String? _cachedPhotoUrl; @override void initState() { super.initState(); _loadQuizResult(); _loadInitialProfile(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _maybeStartPendingQuiz(); }); } Future _maybeStartPendingQuiz() async { try { final prefs = await SharedPreferences.getInstance(); String scopeId = (prefs.getString(_kPendingQuizScopeKey) ?? '').trim(); // O AuthGate pode trocar para o LoggedHome ANTES do register sheet terminar // de gravar a key. Então tentamos por um curto período. int tries = 0; while (scopeId.isEmpty && tries < 12) { await Future.delayed(const Duration(milliseconds: 250)); scopeId = (prefs.getString(_kPendingQuizScopeKey) ?? '').trim(); tries++; } if (scopeId.isEmpty) return; // Limpa antes de navegar para evitar loop se o usuário voltar. await prefs.remove(_kPendingQuizScopeKey); if (!mounted) return; await Navigator.of(context).push( MaterialPageRoute(builder: (_) => Quiz1Screen(scopeId: scopeId)), ); if (!mounted) return; await _loadQuizResult(); } catch (_) { // no-op } } Future _loadInitialProfile() async { final uid = (supabase.auth.currentUser?.id ?? '').trim(); if (uid.isEmpty) return; try { final userDoc = await supabase .from('profiles') .select() .eq('id', uid) .maybeSingle(); final storedName = (userDoc?['name'] ?? '').toString().trim(); final storedPhotoUrl = (userDoc?['photo_url'] ?? '').toString().trim(); final childrenSnap = await supabase .from('children') .select() .eq('owner_id', uid) .order('created_at') .limit(1); String? childName; String? scopeId; if (childrenSnap.isNotEmpty) { final c = childrenSnap.first; final childId = (c['id'] ?? '').toString().trim(); childName = (c['name'] ?? '').toString().trim(); scopeId = '${uid}_$childId'; } if (!mounted) return; setState(() { _cachedUserName = storedName.isNotEmpty ? storedName : _cachedUserName; if (storedPhotoUrl.isNotEmpty) _cachedPhotoUrl = storedPhotoUrl; if ((_selectedChildName ?? '').trim().isEmpty && (childName ?? '').trim().isNotEmpty) { _selectedChildName = childName; } if ((_selectedChildScopeId ?? '').trim().isEmpty && (scopeId ?? '').trim().isNotEmpty) { _selectedChildScopeId = scopeId; } }); await _loadQuizResult(); } catch (_) { // no-op } } Future _loadQuizResult() async { final scope = (_selectedChildScopeId ?? '').trim(); final uid = supabase.auth.currentUser?.id; final String? userId = (uid ?? '').trim().isEmpty ? null : uid; int? score; int? max; if (scope.isNotEmpty && userId != null) { final String childId = scope.startsWith('${userId}_') ? scope.substring(userId.length + 1) : ''; if (childId.trim().isNotEmpty) { try { final childDoc = await supabase .from('children') .select() .eq('id', childId) .maybeSingle(); final s = childDoc?['last_score']; final m = childDoc?['last_max_score']; if (s is int && m is int) { score = s; max = m; } } catch (_) { // no-op } } } if (score != null && max != null) { if (!mounted) return; setState(() { _lastScore = score; _lastMaxScore = max; }); return; } if (scope.isNotEmpty) { score = await QuizPrefs.getLastScoreForScope(scope); max = await QuizPrefs.getLastMaxScoreForScope(scope); } else if (userId != null) { score = await QuizPrefs.getLastScoreForUser(userId); max = await QuizPrefs.getLastMaxScoreForUser(userId); } else { score = await QuizPrefs.getLastScore(); max = await QuizPrefs.getLastMaxScore(); } if (!mounted) return; setState(() { _lastScore = score; _lastMaxScore = max; }); } void selectChild(String? name, String? scopeId) { setState(() { _selectedChildName = name; _selectedChildScopeId = scopeId; }); _loadQuizResult(); } void updateCachedPhoto(String url) { setState(() => _cachedPhotoUrl = url); } String _greeting() { final hour = DateTime.now().hour; if (hour < 12) return 'Bom dia'; if (hour < 18) return 'Boa tarde'; return 'Boa noite'; } @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); final int? score = _lastScore; final int? maxScore = _lastMaxScore; final bool hasScore = score != null && maxScore != null && maxScore > 0; final int percent = hasScore ? ((score / maxScore) * 100).round() : 0; final double appBarHeight = _index == 0 ? (hasScore ? _expandedAppBarHeight : _nameOnlyAppBarHeight) : _collapsedAppBarHeight; final double toolbarHeight = _index == 0 ? kToolbarHeight : appBarHeight; final String title = _index == 0 ? '' : _index == 1 ? 'Perfil' : 'Configurações'; final ShapeBorder appBarShape = _index == 0 ? const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(bottom: Radius.circular(40)), ) : const RoundedRectangleBorder(borderRadius: BorderRadius.zero); final shownName = _cachedUserName; final double bodyTopPadding = _index == 0 ? 0 : 10; return Scaffold( appBar: PreferredSize( preferredSize: Size.fromHeight(appBarHeight), child: AnimatedSize( duration: const Duration(milliseconds: 320), curve: Curves.easeOutCubic, alignment: Alignment.topCenter, child: SizedBox( height: appBarHeight, child: AppBar( toolbarHeight: toolbarHeight, clipBehavior: Clip.antiAlias, flexibleSpace: ClipRRect( borderRadius: BorderRadius.vertical( bottom: Radius.circular(_index == 0 ? 40 : 0), ), child: Container( decoration: const BoxDecoration(gradient: kAppBarGradient), child: _index != 0 ? null : Stack( fit: StackFit.expand, children: [ Opacity( opacity: 0.22, child: Transform.scale(scale: 1.25), ), if (hasScore) Positioned( left: 0, right: 0, top: toolbarHeight + 18, child: Center( child: Text( (_selectedChildName ?? '').trim(), textAlign: TextAlign.center, style: TextStyle( fontWeight: FontWeight.w800, color: Colors.white.withValues( alpha: 0.92, ), fontSize: 14, ), ), ), ) else if ((_selectedChildName ?? '') .trim() .isNotEmpty) Positioned( left: 0, right: 0, top: toolbarHeight, bottom: 0, child: Center( child: Text( _selectedChildName!.trim(), textAlign: TextAlign.center, style: TextStyle( fontWeight: FontWeight.w800, color: Colors.white.withValues( alpha: 0.92, ), fontSize: 14, ), ), ), ), if (hasScore) Positioned( left: 0, right: 0, bottom: 6, child: Center( child: _RiskArcGauge(percent: percent), ), ), ], ), ), ), title: Align( alignment: _index == 0 ? Alignment.topLeft : Alignment.center, child: _index == 0 ? Padding( padding: const EdgeInsets.only(left: 16, right: 10), child: TapBounce( scale: 0.96, child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(30), onTap: () => setState(() => _index = 1), child: Padding( padding: const EdgeInsets.symmetric( vertical: 4, horizontal: 4, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ CircleAvatar( radius: 20, backgroundColor: Colors.white.withValues( alpha: 0.25, ), backgroundImage: (_cachedPhotoUrl ?? '').isNotEmpty ? NetworkImage(_cachedPhotoUrl!) : null, child: (_cachedPhotoUrl ?? '').isEmpty ? const Icon( Icons.person_rounded, color: Colors.white, ) : null, ), const SizedBox(width: 10), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( _greeting(), style: TextStyle( fontWeight: FontWeight.w600, color: Colors.white.withValues( alpha: 0.85, ), fontSize: 12, ), ), Text( shownName, textAlign: TextAlign.left, style: const TextStyle( fontWeight: FontWeight.w900, color: Colors.white, fontSize: 19, ), ), ], ), ], ), ), ), ), ), ) : Text( title, textAlign: TextAlign.center, style: const TextStyle( fontWeight: FontWeight.w900, color: Colors.white, ), ), ), centerTitle: _index == 0 ? false : true, backgroundColor: _teal, foregroundColor: Colors.white, surfaceTintColor: _teal, elevation: 0, shape: appBarShape, ), ), ), ), body: Stack( clipBehavior: Clip.none, children: [ Positioned.fill(child: Container(color: const Color(0xFFFFE6F1))), Positioned( left: -size.width * 0.40, bottom: -size.width * 0.45, child: IgnorePointer( child: SizedBox( width: size.width * 1.05, height: size.width * 1.05, child: Transform.rotate( angle: 35 * math.pi / 180, child: Opacity( opacity: 0.95, child: Lottie.asset( 'lottie/Liquid waves.json', fit: BoxFit.cover, repeat: true, ), ), ), ), ), ), SafeArea( top: false, child: Align( alignment: Alignment.center, child: Padding( padding: EdgeInsets.fromLTRB(16, bodyTopPadding, 16, 16), child: _index == 0 ? _InicioTab(onQuizClosed: _loadQuizResult) : _index == 1 ? _PerfilTab( selectedChildIndex: _selectedChildIndex, onChildSelected: (index, name, scopeId) { setState(() { _selectedChildIndex = index; _selectedChildName = name; _selectedChildScopeId = scopeId; }); _loadQuizResult(); }, ) : const SettingsBody(), ), ), ), ], ), bottomNavigationBar: BottomNavigationBar( currentIndex: _index, onTap: (i) { if (i == _index) return; HapticFeedback.selectionClick(); setState(() => _index = i); }, backgroundColor: const Color(0xFFFFE6F1), selectedItemColor: _teal, unselectedItemColor: Colors.black54, type: BottomNavigationBarType.fixed, items: [ BottomNavigationBarItem( icon: AnimatedNavIcon( icon: Icons.home_rounded, selected: _index == 0, ), label: 'Início', ), BottomNavigationBarItem( icon: AnimatedNavIcon( icon: Icons.person_rounded, selected: _index == 1, ), label: 'Perfil', ), BottomNavigationBarItem( icon: AnimatedNavIcon( icon: Icons.settings_rounded, selected: _index == 2, ), label: 'Ajustes', ), ], ), ); } } class _RiskArcGauge extends StatelessWidget { const _RiskArcGauge({required this.percent}); final int percent; @override Widget build(BuildContext context) { final clamped = percent.clamp(0, 100); return TweenAnimationBuilder( duration: const Duration(milliseconds: 700), curve: Curves.easeOutCubic, tween: Tween(begin: 0, end: clamped / 100), builder: (context, value, _) { final shown = (value * 100).round(); return SizedBox( width: 120, height: 60, child: Stack( clipBehavior: Clip.none, children: [ Positioned.fill( child: CustomPaint( painter: _RiskArcGaugePainter(progress: value), ), ), Positioned( top: 38, left: 6, right: 0, child: Center( child: Text( '$shown%', style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.w900, height: 1, ), ), ), ), ], ), ); }, ); } } class _RiskArcGaugePainter extends CustomPainter { const _RiskArcGaugePainter({required this.progress}); final double progress; @override void paint(Canvas canvas, Size size) { final rect = Rect.fromLTWH(10, 8, size.width - 20, size.height * 1.7); const startAngle = math.pi; const sweepAngle = math.pi; final strokeWidth = size.width * 0.12; final backgroundPaint = Paint() ..color = Colors.white.withValues(alpha: 0.72) ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth ..strokeCap = StrokeCap.butt; final progressPaint = Paint() ..color = const Color(0xFFFF9AD0) ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth ..strokeCap = StrokeCap.butt; canvas.drawArc(rect, startAngle, sweepAngle, false, backgroundPaint); canvas.drawArc( rect, startAngle, sweepAngle * progress.clamp(0, 1), false, progressPaint, ); } @override bool shouldRepaint(covariant _RiskArcGaugePainter oldDelegate) { return oldDelegate.progress != progress; } } class _InicioTab extends StatelessWidget { const _InicioTab({required this.onQuizClosed}); final VoidCallback onQuizClosed; Future _startQuiz(BuildContext context) async { final uid = (supabase.auth.currentUser?.id ?? '').trim(); if (uid.isEmpty) return; List> children = const []; try { children = await supabase .from('children') .select() .eq('owner_id', uid) .order('created_at'); } catch (_) { // segue com lista vazia; tratado abaixo } if (!context.mounted) return; Map? chosen; if (children.isEmpty) { chosen = await _requireFirstChild(context, uid); if (chosen == null) return; } else if (children.length == 1) { chosen = children.first; } else { if (!context.mounted) return; chosen = await _pickChildSheet(context, children); if (chosen == null) return; } final childId = (chosen['id'] ?? '').toString().trim(); final childName = (chosen['name'] ?? '').toString().trim(); final scopeId = childId.isEmpty ? uid : '${uid}_$childId'; if (!context.mounted) return; final state = context.findAncestorStateOfType<_LoggedHomeScreenState>(); state?.selectChild(childName, scopeId); await Navigator.of(context).push( MaterialPageRoute(builder: (_) => Quiz1Screen(scopeId: scopeId)), ); onQuizClosed(); } @override Widget build(BuildContext context) { final state = context.findAncestorStateOfType<_LoggedHomeScreenState>(); final selectedChildName = (state?._selectedChildName ?? '').trim(); return Align( alignment: Alignment.topCenter, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 560), child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.only(top: 18, bottom: 16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ FadeSlideIn( child: TapBounce( scale: 0.97, child: _HeroQuizCard( childName: selectedChildName, onStartQuiz: () => _startQuiz(context), ), ), ), const SizedBox(height: 16), FadeSlideIn( delay: const Duration(milliseconds: 90), child: TapBounce( scale: 0.97, child: _VideoLibraryCard( onOpenLibrary: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => const VideoScreen(), ), ); }, ), ), ), const SizedBox(height: 16), ], ), ), ), ), ); } } Future?> _createChildViaSheet( BuildContext context, String uid, ) async { final result = await showModalBottomSheet?>( context: context, isScrollControlled: true, showDragHandle: true, backgroundColor: const Color(0xFFFFE6F1), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (ctx) => const _AddChildSheet(), ); if (result == null) return null; if (!context.mounted) return null; try { final inserted = await supabase .from('children') .insert({...result, 'owner_id': uid}) .select() .single(); return inserted; } catch (e) { if (!context.mounted) return null; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Erro ao adicionar criança: $e'))); return null; } } Future?> _requireFirstChild( BuildContext context, String uid, ) async { final proceed = await showConfirmDialog( context, title: 'Cadastre uma criança', message: 'Antes de iniciar o quiz, adicione uma criança ao seu perfil.', confirmLabel: 'Adicionar criança', ); if (proceed != true) return null; if (!context.mounted) return null; return _createChildViaSheet(context, uid); } Future?> _pickChildSheet( BuildContext context, List> children, ) { const Color teal = Color(0xFF2F9E94); return showModalBottomSheet?>( context: context, isScrollControlled: true, showDragHandle: true, backgroundColor: const Color(0xFFFFE6F1), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (ctx) { final maxHeight = MediaQuery.sizeOf(ctx).height * 0.8; return SafeArea( child: ConstrainedBox( constraints: BoxConstraints(maxHeight: maxHeight), child: Padding( padding: const EdgeInsets.fromLTRB(18, 6, 18, 18), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( 'Para qual criança é o quiz?', textAlign: TextAlign.center, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w900, color: Color(0xFFFF55A7), ), ), const SizedBox(height: 14), Flexible( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: children.map((c) { final name = (c['name'] ?? '').toString(); final age = c['age']; final label = age != null ? '$name • $age anos' : name; return Padding( padding: const EdgeInsets.only(bottom: 10), child: TapBounce( scale: 0.97, child: Material( color: Colors.white.withValues(alpha: 0.85), borderRadius: BorderRadius.circular(16), child: InkWell( borderRadius: BorderRadius.circular(16), onTap: () => Navigator.of(ctx).pop(c), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 14, ), child: Row( children: [ Expanded( child: Text( label, style: const TextStyle( fontWeight: FontWeight.w800, ), ), ), const Icon( Icons.chevron_right_rounded, color: teal, ), ], ), ), ), ), ), ); }).toList(), ), ), ), const SizedBox(height: 4), TextButton( onPressed: () => Navigator.of(ctx).pop(null), child: const Text('Cancelar'), ), ], ), ), ), ); }, ); } class _HeroQuizCard extends StatelessWidget { const _HeroQuizCard({required this.childName, required this.onStartQuiz}); final String childName; final VoidCallback onStartQuiz; @override Widget build(BuildContext context) { const Color teal = Color(0xFF2F9E94); const Color pink = Color(0xFFFF55A7); return Material( elevation: 12, shadowColor: Colors.black.withValues(alpha: 0.18), borderRadius: BorderRadius.circular(24), color: Colors.white, child: Padding( padding: const EdgeInsets.fromLTRB(20, 20, 20, 18), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: teal.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(14), ), child: const Icon( Icons.medical_services_rounded, color: teal, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Avaliação de saúde oral', style: TextStyle( fontWeight: FontWeight.w900, fontSize: 16, color: pink, ), ), const SizedBox(height: 2), Text( childName.isNotEmpty ? 'Para $childName' : 'Responda o quiz e descubra como cuidar melhor do sorriso.', style: TextStyle( color: Colors.black.withValues(alpha: 0.62), fontWeight: FontWeight.w600, fontSize: 13, ), ), ], ), ), ], ), const SizedBox(height: 16), SizedBox( height: 48, width: double.infinity, child: FilledButton.icon( style: FilledButton.styleFrom( backgroundColor: pink, foregroundColor: Colors.white, shape: const StadiumBorder(), textStyle: const TextStyle( fontWeight: FontWeight.w800, fontSize: 15, ), ), onPressed: onStartQuiz, icon: const Icon(Icons.play_arrow_rounded), label: const Text('Iniciar Quiz'), ), ), ], ), ), ); } } class _VideoLibraryCard extends StatelessWidget { const _VideoLibraryCard({required this.onOpenLibrary}); final VoidCallback onOpenLibrary; @override Widget build(BuildContext context) { final featured = videoList.firstWhere( (v) => v.videoPath != null, orElse: () => videoList.first, ); return Material( color: Colors.white, borderRadius: BorderRadius.circular(24), elevation: 12, shadowColor: Colors.black.withValues(alpha: 0.18), child: InkWell( borderRadius: BorderRadius.circular(24), onTap: onOpenLibrary, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ClipRRect( borderRadius: const BorderRadius.vertical( top: Radius.circular(24), ), child: SizedBox( height: 160, child: Stack( fit: StackFit.expand, children: [ Container( color: const Color(0xFF2F9E94).withValues(alpha: 0.14), ), VideoThumbnail( video: featured, borderRadius: 0, iconSize: 54, ), IgnorePointer( child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, Colors.black.withValues(alpha: 0.35), ], stops: const [0.6, 1.0], ), ), ), ), ], ), ), ), Padding( padding: const EdgeInsets.fromLTRB(18, 14, 14, 14), child: Row( children: [ Container( width: 38, height: 38, decoration: BoxDecoration( color: const Color(0xFF2F9E94).withValues(alpha: 0.12), borderRadius: BorderRadius.circular(12), ), child: const Icon( Icons.video_library_rounded, color: Color(0xFF2F9E94), size: 20, ), ), const SizedBox(width: 12), const Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Vídeos Educativos', style: TextStyle( fontWeight: FontWeight.w900, fontSize: 15, color: Color(0xFFFF55A7), ), ), SizedBox(height: 2), Text( 'Aprenda mais sobre saúde oral', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.black54, ), ), ], ), ), const Icon( Icons.chevron_right_rounded, color: Color(0xFF2F9E94), size: 24, ), ], ), ), ], ), ), ); } } class _PerfilTab extends StatefulWidget { const _PerfilTab({ required this.selectedChildIndex, required this.onChildSelected, }); final int selectedChildIndex; final void Function(int index, String? name, String? scopeId) onChildSelected; @override State<_PerfilTab> createState() => _PerfilTabState(); } class _PerfilTabState extends State<_PerfilTab> { bool _addingChild = false; bool _updatingPhoto = false; bool _initialLoading = true; Map? _profileData; List> _children = const []; @override void initState() { super.initState(); _loadPerfilData().whenComplete(() { if (mounted) setState(() => _initialLoading = false); }); } // Busca simples (sem Realtime): o Realtime do Supabase pode travar depois de // várias trocas de aba/reconexões, deixando a lista de filhos e a foto sem // atualizar. Como só o próprio usuário edita esses dados, buscamos uma vez // e recarregamos manualmente após cada ação (adicionar/remover filho, trocar // foto), o que é bem mais confiável. Future _loadPerfilData() async { final uid = (supabase.auth.currentUser?.id ?? '').trim(); if (uid.isEmpty) return; try { final profile = await supabase .from('profiles') .select() .eq('id', uid) .maybeSingle(); final children = await supabase .from('children') .select() .eq('owner_id', uid) .order('created_at'); if (!mounted) return; setState(() { _profileData = profile; _children = children; }); } catch (_) { // mantém os dados já carregados; usuário pode tentar de novo } } Future<(int?, int?)> _loadScoreForScope(String scopeId) async { final score = await QuizPrefs.getLastScoreForScope(scopeId); final max = await QuizPrefs.getLastMaxScoreForScope(scopeId); return (score, max); } Future _pickAndUploadProfilePhoto( BuildContext context, String uid, ) async { if (_updatingPhoto) return; final source = await showModalBottomSheet( context: context, showDragHandle: true, backgroundColor: const Color(0xFFFFE6F1), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (ctx) { return SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(18, 6, 18, 18), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( 'Foto de perfil', textAlign: TextAlign.center, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w900, color: Color(0xFFFF55A7), ), ), const SizedBox(height: 14), ClipRRect( borderRadius: BorderRadius.circular(999), child: DecoratedBox( decoration: const BoxDecoration( gradient: kGreenButtonGradient, ), child: SizedBox( height: 46, child: FilledButton( style: FilledButton.styleFrom( backgroundColor: Colors.transparent, foregroundColor: Colors.white, shape: const StadiumBorder(), textStyle: const TextStyle( fontWeight: FontWeight.w900, ), ), onPressed: () => Navigator.of(ctx).pop(ImageSource.camera), child: const Text('Câmera'), ), ), ), ), const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular(999), child: DecoratedBox( decoration: const BoxDecoration( gradient: kGreenButtonGradient, ), child: SizedBox( height: 46, child: FilledButton( style: FilledButton.styleFrom( backgroundColor: Colors.transparent, foregroundColor: Colors.white, shape: const StadiumBorder(), textStyle: const TextStyle( fontWeight: FontWeight.w900, ), ), onPressed: () => Navigator.of(ctx).pop(ImageSource.gallery), child: const Text('Galeria'), ), ), ), ), const SizedBox(height: 8), SizedBox( height: 42, child: TextButton( style: TextButton.styleFrom( foregroundColor: const Color(0xFF2F9E94), ), onPressed: () => Navigator.of(ctx).pop(), child: const Text('Cancelar'), ), ), ], ), ), ); }, ); if (source == null) return; final picker = ImagePicker(); final picked = await picker.pickImage( source: source, imageQuality: 82, maxWidth: 1024, ); if (picked == null) return; setState(() => _updatingPhoto = true); try { final file = File(picked.path); final path = '$uid/profile.jpg'; await supabase.storage .from('photos') .upload(path, file, fileOptions: const FileOptions(upsert: true)); final publicUrl = supabase.storage.from('photos').getPublicUrl(path); final url = '$publicUrl?t=${DateTime.now().millisecondsSinceEpoch}'; await supabase.from('profiles').upsert({'id': uid, 'photo_url': url}); await _loadPerfilData(); if (context.mounted) { context .findAncestorStateOfType<_LoggedHomeScreenState>() ?.updateCachedPhoto(url); } } catch (e) { if (!context.mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Erro ao enviar foto: $e'))); } finally { if (mounted) setState(() => _updatingPhoto = false); } } Future _confirmDeleteChild( BuildContext context, { required String childId, required String childName, }) async { final messenger = ScaffoldMessenger.of(context); final confirmed = await showConfirmDialog( context, title: 'Remover criança', message: 'Tem certeza que deseja remover "$childName"? Essa ação não pode ser desfeita.', confirmLabel: 'Remover', confirmColor: const Color(0xFFFF55A7), ); if (confirmed != true) return; if (!context.mounted) return; try { final deleted = await supabase .from('children') .delete() .eq('id', childId) .select('id'); if (deleted.isEmpty) { throw StateError('Sem permissão para remover esta criança.'); } widget.onChildSelected(0, null, null); await _loadPerfilData(); messenger.showSnackBar(const SnackBar(content: Text('Criança removida'))); } catch (e) { messenger.showSnackBar(SnackBar(content: Text('Erro ao remover: $e'))); } } Future _addAnotherChild(BuildContext context, String uid) async { if (_addingChild) return; final messenger = ScaffoldMessenger.of(context); final result = await showModalBottomSheet?>( context: context, isScrollControlled: true, showDragHandle: true, backgroundColor: const Color(0xFFFFE6F1), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (ctx) => const _AddChildSheet(), ); if (!mounted) return; if (result == null) return; final childMap = {...result, 'owner_id': uid}; setState(() => _addingChild = true); try { await supabase .from('children') .insert(childMap) .timeout(const Duration(seconds: 20)); if (!mounted) return; await _loadPerfilData(); messenger.showSnackBar( const SnackBar(content: Text('Criança adicionada')), ); if (mounted) { setState(() => _addingChild = false); } if (!mounted) return; if (!mounted) return; final addMore = await showConfirmDialog( // ignore: use_build_context_synchronously context, title: 'Adicionar outra criança?', cancelLabel: 'Agora não', confirmLabel: 'Adicionar outra', ); if (!mounted) return; if (addMore == true) { await Future.delayed(const Duration(milliseconds: 120)); if (!mounted) return; if (!mounted) return; if (!mounted) return; // ignore: use_build_context_synchronously await _addAnotherChild(context, uid); } } on TimeoutException { if (!mounted) return; messenger.showSnackBar( const SnackBar( content: Text('Tempo esgotado ao adicionar. Tente novamente.'), ), ); } catch (e) { if (!mounted) return; messenger.showSnackBar(SnackBar(content: Text('Erro ao adicionar: $e'))); } finally { if (mounted) setState(() => _addingChild = false); } } @override Widget build(BuildContext context) { final user = supabase.auth.currentUser; final uid = (user?.id ?? '').trim(); final name = (user?.userMetadata?['name'] ?? '').toString().trim(); final email = (user?.email ?? '').trim(); final shownName = name.isNotEmpty ? name : 'Sem nome'; if (uid.isEmpty) { return const SizedBox.shrink(); } if (_initialLoading) { return const Center( child: Padding( padding: EdgeInsets.only(top: 60), child: CircularProgressIndicator(color: Color(0xFF2F9E94)), ), ); } final data = _profileData; final storedName = (data?['name'] ?? '').toString().trim(); final profileName = storedName.isNotEmpty ? storedName : shownName; final photoUrl = (data?['photo_url'] ?? '').toString().trim(); final storedEmail = (data?['email'] ?? '').toString().trim(); final profileEmail = storedEmail.isNotEmpty ? storedEmail : email; final children = _children; final int selectedIndex = children.isEmpty ? 0 : widget.selectedChildIndex.clamp( 0, (children.length - 1).clamp(0, 999999), ); return Align( alignment: Alignment.topCenter, child: Padding( padding: const EdgeInsets.only(top: 10), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 560), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ FadeSlideIn( child: Material( elevation: 10, color: Colors.white, borderRadius: BorderRadius.circular(20), shadowColor: Colors.black.withValues(alpha: 0.16), child: Padding( padding: const EdgeInsets.all(18), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ TapBounce( scale: 0.92, child: InkWell( borderRadius: BorderRadius.circular(40), onTap: _updatingPhoto ? null : () => _pickAndUploadProfilePhoto( context, uid, ), child: Stack( clipBehavior: Clip.none, children: [ Container( width: 76, height: 76, decoration: BoxDecoration( color: const Color(0xFFFFE6F1), shape: BoxShape.circle, border: Border.all( color: const Color( 0xFF2F9E94, ).withValues(alpha: 0.35), width: 2, ), ), clipBehavior: Clip.antiAlias, child: Stack( fit: StackFit.expand, children: [ if (photoUrl.isNotEmpty) Image.network( photoUrl, fit: BoxFit.cover, ) else const Icon( Icons.person_rounded, size: 42, color: Color(0xFF2F9E94), ), if (_updatingPhoto) Container( color: Colors.black.withValues( alpha: 0.25, ), child: const Center( child: SizedBox( width: 22, height: 22, child: CircularProgressIndicator( strokeWidth: 2, ), ), ), ), ], ), ), Positioned( right: -2, bottom: -2, child: Container( width: 26, height: 26, decoration: const BoxDecoration( color: Color(0xFFFF55A7), shape: BoxShape.circle, ), child: const Icon( Icons.camera_alt_rounded, size: 14, color: Colors.white, ), ), ), ], ), ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( profileName, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w900, color: Color(0xFFFF55A7), ), ), if (profileEmail.isNotEmpty) ...[ const SizedBox(height: 4), Text( profileEmail, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: Colors.black.withValues( alpha: 0.55, ), ), ), ], ], ), ), ], ), ), ), ), const SizedBox(height: 22), Padding( padding: const EdgeInsets.only(left: 4, bottom: 10), child: Row( children: [ const Text( 'Meus filhos', style: TextStyle( color: Color(0xFF2F9E94), fontWeight: FontWeight.w900, fontSize: 15, ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: const Color( 0xFF2F9E94, ).withValues(alpha: 0.10), borderRadius: BorderRadius.circular(999), ), child: Text( '${children.length}', style: const TextStyle( color: Color(0xFF2F9E94), fontWeight: FontWeight.w900, fontSize: 12, ), ), ), ], ), ), if (children.isEmpty) Container( padding: const EdgeInsets.all(18), margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all( color: Colors.black.withValues(alpha: 0.08), ), ), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: const Color(0xFFFFE6F1), borderRadius: BorderRadius.circular(12), ), child: const Icon( Icons.child_care_rounded, color: Color(0xFFFF55A7), ), ), const SizedBox(width: 12), Expanded( child: Text( 'Nenhuma criança adicionada ainda.', style: TextStyle( color: Colors.black.withValues(alpha: 0.62), fontWeight: FontWeight.w600, ), ), ), ], ), ) else ...children.asMap().entries.map((entry) { final i = entry.key; final c = entry.value; final childId = (c['id'] ?? '').toString().trim(); final childName = (c['name'] ?? '').toString().trim(); final childAge = c['age']; final childGender = (c['gender'] ?? '').toString().trim(); final scopeId = '${uid}_$childId'; final title = childName.isNotEmpty ? childName : 'Criança ${i + 1}'; final subtitle = [ if (childAge != null) 'Idade: $childAge', if (childGender.isNotEmpty) 'Gênero: $childGender', ].join(' • '); final bool selected = i == selectedIndex; return FadeSlideIn( delay: Duration(milliseconds: 60 * i.clamp(0, 6)), child: Padding( padding: const EdgeInsets.only(bottom: 12), child: TapBounce( scale: 0.97, child: InkWell( borderRadius: BorderRadius.circular(16), onTap: () => widget.onChildSelected( i, childName.isEmpty ? null : childName, scopeId, ), child: Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: selected ? const Color(0xFFFFE6F1) : Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all( color: selected ? const Color( 0xFF2F9E94, ).withValues(alpha: 0.45) : Colors.black.withValues(alpha: 0.10), width: selected ? 1.6 : 1, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.06), blurRadius: 14, offset: const Offset(0, 8), ), ], ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontWeight: FontWeight.w900, ), ), if (subtitle.isNotEmpty) ...[ const SizedBox(height: 4), Text(subtitle), ], ], ), ), const SizedBox(width: 10), FutureBuilder<(int?, int?)>( future: _loadScoreForScope(scopeId), builder: (context, snap) { final tuple = snap.data; final s = tuple?.$1; final m = tuple?.$2; final text = (s == null || m == null || m <= 0) ? '--' : '${(((s / m) * 100).round()).clamp(0, 100)}%'; return Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 8, ), decoration: BoxDecoration( color: const Color( 0xFF2F9E94, ).withValues(alpha: 0.10), borderRadius: BorderRadius.circular( 999, ), ), child: Text( text, style: const TextStyle( fontWeight: FontWeight.w900, color: Color(0xFF2F9E94), ), ), ); }, ), const SizedBox(width: 6), IconButton( onPressed: () => _confirmDeleteChild( context, childId: childId, childName: title, ), icon: const Icon( Icons.delete_outline_rounded, color: Color(0xFFFF55A7), ), tooltip: 'Remover', visualDensity: VisualDensity.compact, ), ], ), ), ), ), ), ); }), TapBounce( child: ClipRRect( borderRadius: BorderRadius.circular(999), child: DecoratedBox( decoration: const BoxDecoration( gradient: kGreenButtonGradient, ), child: SizedBox( height: 48, child: FilledButton.icon( style: FilledButton.styleFrom( backgroundColor: Colors.transparent, foregroundColor: Colors.white, shape: const StadiumBorder(), textStyle: const TextStyle( fontWeight: FontWeight.w800, ), ), onPressed: _addingChild ? null : () => _addAnotherChild(context, uid), icon: const Icon(Icons.add_rounded), label: const Text('Adicionar criança'), ), ), ), ), ), const SizedBox(height: 22), TapBounce( child: SizedBox( height: 46, child: OutlinedButton.icon( style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFFFF55A7), side: const BorderSide( color: Color(0xFFFF55A7), width: 1.4, ), shape: const StadiumBorder(), textStyle: const TextStyle(fontWeight: FontWeight.w800), ), onPressed: () async { await supabase.auth.signOut(); }, icon: const Icon(Icons.logout_rounded), label: const Text('Sair'), ), ), ), const SizedBox(height: 12), ], ), ), ), ), ); } } class _AddChildSheet extends StatefulWidget { const _AddChildSheet(); @override State<_AddChildSheet> createState() => _AddChildSheetState(); } class _AddChildSheetState extends State<_AddChildSheet> { final _formKey = GlobalKey(); final _nameController = TextEditingController(); final _ageController = TextEditingController(); String? _gender; @override void dispose() { _nameController.dispose(); _ageController.dispose(); super.dispose(); } void _submit() { if (!(_formKey.currentState?.validate() ?? false)) return; Navigator.of(context).pop({ 'name': _nameController.text.trim(), 'age': int.parse(_ageController.text.trim()), 'gender': (_gender ?? '').trim(), }); } @override Widget build(BuildContext context) { final bottomInset = MediaQuery.viewInsetsOf(context).bottom; return SafeArea( child: Padding( padding: EdgeInsets.fromLTRB(18, 6, 18, 18 + bottomInset), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( 'Adicionar outra criança', textAlign: TextAlign.center, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w900, color: Color(0xFFFF55A7), ), ), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.82), borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.black.withValues(alpha: 0.08)), ), child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( controller: _nameController, textInputAction: TextInputAction.next, decoration: const InputDecoration( labelText: 'Nome da criança', ), validator: (v) { final value = (v ?? '').trim(); if (value.isEmpty) return 'Informe o nome'; if (value.length < 2) return 'Nome muito curto'; if (!_namePattern.hasMatch(value)) { return 'O nome não pode conter números'; } return null; }, ), TextFormField( controller: _ageController, keyboardType: TextInputType.number, textInputAction: TextInputAction.next, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(2), ], decoration: const InputDecoration(labelText: 'Idade'), validator: (v) { final raw = (v ?? '').trim(); if (raw.isEmpty) return 'Informe a idade'; final age = int.tryParse(raw); if (age == null) return 'Idade inválida'; if (age < 1 || age > 17) { return 'Idade deve ser entre 1 e 17 anos'; } return null; }, ), DropdownButtonFormField( initialValue: _gender, items: const [ DropdownMenuItem( value: 'Masculino', child: Text('Masculino'), ), DropdownMenuItem( value: 'Feminino', child: Text('Feminino'), ), DropdownMenuItem(value: 'Outro', child: Text('Outro')), ], onChanged: (v) => setState(() => _gender = v), decoration: const InputDecoration(labelText: 'Gênero'), validator: (v) { if (v == null || v.trim().isEmpty) { return 'Selecione o gênero'; } return null; }, ), ], ), ), ), const SizedBox(height: 14), Row( children: [ Expanded( child: SizedBox( height: 44, child: TextButton( onPressed: () => Navigator.of(context).pop(null), child: const Text('Cancelar'), ), ), ), const SizedBox(width: 10), Expanded( child: TapBounce( child: ClipRRect( borderRadius: BorderRadius.circular(999), child: DecoratedBox( decoration: const BoxDecoration( gradient: kGreenButtonGradient, ), child: SizedBox( height: 44, child: FilledButton( style: FilledButton.styleFrom( backgroundColor: Colors.transparent, foregroundColor: Colors.white, shape: const StadiumBorder(), textStyle: const TextStyle( fontWeight: FontWeight.w900, ), ), onPressed: _submit, child: const Text('Adicionar'), ), ), ), ), ), ), ], ), ], ), ), ); } }