import 'package:firebase_auth/firebase_auth.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:lottie/lottie.dart'; import 'dart:async'; import 'package:youtube_player_flutter/youtube_player_flutter.dart'; import 'dart:math' as math; import 'dart:io'; import 'package:shared_preferences/shared_preferences.dart'; import 'quiz/quiz1.dart'; import 'quiz/quiz_prefs.dart'; import 'screens/video_screen.dart'; 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 = 104; static const double _expandedAppBarHeight = 180; int _index = 0; int _selectedChildIndex = 0; String? _selectedChildName; String? _selectedChildScopeId; int? _lastScore; int? _lastMaxScore; String _cachedUserName = 'Sem nome'; @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 = (FirebaseAuth.instance.currentUser?.uid ?? '').trim(); if (uid.isEmpty) return; try { final userDoc = await FirebaseFirestore.instance .collection('users') .doc(uid) .get(); final data = userDoc.data(); final storedName = (data?['name'] ?? '').toString().trim(); QuerySnapshot> childrenSnap; try { childrenSnap = await FirebaseFirestore.instance .collection('users') .doc(uid) .collection('children') .orderBy('createdAt', descending: false) .limit(1) .get(); } catch (_) { childrenSnap = await FirebaseFirestore.instance .collection('users') .doc(uid) .collection('children') .limit(1) .get(); } String? childName; String? scopeId; if (childrenSnap.docs.isNotEmpty) { final c = childrenSnap.docs.first.data(); final childId = (c['id'] ?? childrenSnap.docs.first.id) .toString() .trim(); childName = (c['name'] ?? '').toString().trim(); scopeId = '${uid}_$childId'; } if (!mounted) return; setState(() { _cachedUserName = storedName.isNotEmpty ? storedName : _cachedUserName; 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 = FirebaseAuth.instance.currentUser?.uid; 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 FirebaseFirestore.instance .collection('users') .doc(userId) .collection('children') .doc(childId) .get(); final data = childDoc.data(); final s = data?['lastScore']; final m = data?['lastMaxScore']; 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; }); } @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); final double appBarHeight = _index == 0 ? _expandedAppBarHeight : _collapsedAppBarHeight; final double toolbarHeight = _index == 0 ? kToolbarHeight : appBarHeight; final String title = _index == 0 ? '' : 'Perfil'; final ShapeBorder appBarShape = _index == 0 ? const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(bottom: Radius.circular(24)), ) : const RoundedRectangleBorder(borderRadius: BorderRadius.zero); final shownName = _cachedUserName; 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 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: _index != 0 ? null : Stack( fit: StackFit.expand, children: [ Opacity( opacity: 0.22, child: Transform.scale(scale: 1.25), ), Positioned( left: 0, right: 0, top: toolbarHeight + 26, child: Center( child: RichText( textAlign: TextAlign.center, text: TextSpan( style: TextStyle( fontWeight: FontWeight.w800, color: Colors.white.withValues(alpha: 0.92), fontSize: 14, ), children: [ if (((_selectedChildName ?? '') .trim() .isNotEmpty)) TextSpan(text: _selectedChildName!.trim()), if (((_selectedChildName ?? '') .trim() .isNotEmpty) && hasScore) const WidgetSpan( alignment: PlaceholderAlignment.middle, child: Padding( padding: EdgeInsets.symmetric( horizontal: 12, ), child: Text( '•', style: TextStyle(color: Colors.white), ), ), ), if (hasScore) TextSpan(text: '$score/$maxScore'), ], ), ), ), ), if (hasScore) Positioned( left: 0, right: 0, bottom: 12, 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: Text( 'Olá, $shownName', textAlign: TextAlign.left, style: const TextStyle( fontWeight: FontWeight.w900, color: Colors.white, fontSize: 20, ), ), ) : 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) : _PerfilTab( selectedChildIndex: _selectedChildIndex, onChildSelected: (index, name, scopeId) { setState(() { _selectedChildIndex = index; _selectedChildName = name; _selectedChildScopeId = scopeId; }); _loadQuizResult(); }, ), ), ), ), ], ), bottomNavigationBar: BottomNavigationBar( currentIndex: _index, onTap: (i) => setState(() => _index = i), backgroundColor: const Color(0xFFFFE6F1), selectedItemColor: _teal, unselectedItemColor: Colors.black54, type: BottomNavigationBarType.fixed, items: const [ BottomNavigationBarItem( icon: Icon(Icons.home_rounded), label: 'Início', ), BottomNavigationBarItem( icon: Icon(Icons.person_rounded), label: 'Perfil', ), ], ), ); } } 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: 178, height: 94, child: Stack( alignment: Alignment.center, children: [ Positioned.fill( child: CustomPaint( painter: _RiskArcGaugePainter(progress: value), ), ), Positioned( bottom: 8, child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( '$shown%', style: const TextStyle( color: Colors.white, fontSize: 26, fontWeight: FontWeight.w900, height: 1, ), ), const SizedBox(height: 4), Text( 'Risco de Má Oclusão', style: TextStyle( color: Colors.white.withValues(alpha: 0.92), fontSize: 9, fontWeight: FontWeight.w900, ), ), ], ), ), ], ), ); }, ); } } class _RiskArcGaugePainter extends CustomPainter { const _RiskArcGaugePainter({required this.progress}); final double progress; @override void paint(Canvas canvas, Size size) { final rect = Rect.fromLTWH(16, 12, size.width - 32, size.height * 1.55); const startAngle = math.pi; const sweepAngle = math.pi; final strokeWidth = size.width * 0.14; 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; @override Widget build(BuildContext context) { final uid = (FirebaseAuth.instance.currentUser?.uid ?? '').trim(); final state = context.findAncestorStateOfType<_LoggedHomeScreenState>(); final childScope = (state?._selectedChildScopeId ?? '').trim(); final scopeId = childScope.isNotEmpty ? childScope : (uid.isNotEmpty ? uid : null); 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: [ _HeroQuizCard( childName: selectedChildName, onStartQuiz: () { Navigator.of(context) .push( MaterialPageRoute( builder: (_) => Quiz1Screen(scopeId: scopeId), ), ) .then((_) => onQuizClosed()); }, ), const SizedBox(height: 16), _VideoLibraryCard( onOpenLibrary: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => const VideoScreen(), ), ); }, ), const SizedBox(height: 16), ], ), ), ), ), ); } } 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 item = VideoScreen.library.isEmpty ? null : VideoScreen.library.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: [ if (item == null) Container( height: 160, decoration: BoxDecoration( color: const Color(0xFFFFE6F1), borderRadius: const BorderRadius.vertical( top: Radius.circular(24), ), ), ) else ClipRRect( borderRadius: const BorderRadius.vertical( top: Radius.circular(24), ), child: SizedBox( height: 160, child: Stack( fit: StackFit.expand, children: [ _VideoThumbnail(url: item.url), Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withValues(alpha: 0.05), Colors.black.withValues(alpha: 0.45), ], ), ), ), const Align( alignment: Alignment.center, child: Icon( Icons.play_circle_fill_rounded, size: 54, color: Colors.white, ), ), ], ), ), ), 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 _VideoThumbnail extends StatelessWidget { const _VideoThumbnail({required this.url}); final String url; @override Widget build(BuildContext context) { final id = YoutubePlayer.convertUrlToId(url); final thumb = id == null ? null : 'https://img.youtube.com/vi/$id/0.jpg'; if (thumb == null) { return Container(color: Colors.white.withValues(alpha: 0.12)); } return Image.network( thumb, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => Container(color: Colors.white.withValues(alpha: 0.12)), ); } } 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; 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, builder: (ctx) { return SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( 'Foto de perfil', textAlign: TextAlign.center, style: TextStyle(fontWeight: FontWeight.w900, fontSize: 16), ), const SizedBox(height: 12), SizedBox( height: 46, child: FilledButton( onPressed: () => Navigator.of(ctx).pop(ImageSource.camera), child: const Text('Câmera'), ), ), const SizedBox(height: 10), SizedBox( height: 46, child: FilledButton( onPressed: () => Navigator.of(ctx).pop(ImageSource.gallery), child: const Text('Galeria'), ), ), const SizedBox(height: 10), SizedBox( height: 42, child: TextButton( 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 ref = FirebaseStorage.instance .ref() .child('users') .child(uid) .child('profile.jpg'); await ref.putFile(file); final url = await ref.getDownloadURL(); await FirebaseFirestore.instance.collection('users').doc(uid).set({ 'photoUrl': url, }, SetOptions(merge: true)); } 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 _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 childId = FirebaseFirestore.instance .collection('users') .doc(uid) .collection('children') .doc() .id; final childMap = { ...result, 'id': childId, 'createdAt': FieldValue.serverTimestamp(), }; setState(() => _addingChild = true); try { await FirebaseFirestore.instance .collection('users') .doc(uid) .collection('children') .doc(childId) .set(childMap, SetOptions(merge: true)) .timeout(const Duration(seconds: 20)); if (!mounted) return; messenger.showSnackBar( const SnackBar(content: Text('Criança adicionada')), ); if (mounted) { setState(() => _addingChild = false); } if (!mounted) return; if (!mounted) return; final addMore = await showDialog( // ignore: use_build_context_synchronously context: context, builder: (ctx) { return AlertDialog( title: const Text('Adicionar outra criança?'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Agora não'), ), FilledButton( onPressed: () => Navigator.of(ctx).pop(true), child: const Text('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 = FirebaseAuth.instance.currentUser; final uid = (user?.uid ?? '').trim(); final name = (user?.displayName ?? '').trim(); final email = (user?.email ?? '').trim(); final shownName = name.isNotEmpty ? name : 'Sem nome'; if (uid.isEmpty) { return const SizedBox.shrink(); } return StreamBuilder>>( stream: FirebaseFirestore.instance .collection('users') .doc(uid) .snapshots(), builder: (context, userSnapshot) { final data = userSnapshot.data?.data(); final storedName = (data?['name'] ?? '').toString().trim(); final profileName = storedName.isNotEmpty ? storedName : shownName; final photoUrl = (data?['photoUrl'] ?? '').toString().trim(); final storedEmail = (data?['email'] ?? '').toString().trim(); final profileEmail = storedEmail.isNotEmpty ? storedEmail : email; return StreamBuilder>>( stream: FirebaseFirestore.instance .collection('users') .doc(uid) .collection('children') .orderBy('createdAt', descending: false) .snapshots(), builder: (context, childSnapshot) { final docs = childSnapshot.data?.docs ?? const []; final children = docs.map((d) => d.data()).toList(); 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: [ 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: [ 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().isEmpty ? docs[i].id : (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 Padding( padding: const EdgeInsets.only(bottom: 12), 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), ), ), ); }, ), ], ), ), ), ); }), SizedBox( height: 48, child: FilledButton.icon( style: FilledButton.styleFrom( backgroundColor: const Color(0xFF2F9E94), 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), 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 FirebaseAuth.instance.signOut(); }, icon: const Icon(Icons.logout_rounded), label: const Text('Sair'), ), ), const SizedBox(height: 12), ], ), ), ), ), ); }, ); }, ); } } class _AddChildDialog extends StatefulWidget { const _AddChildDialog(); @override State<_AddChildDialog> createState() => _AddChildDialogState(); } class _AddChildDialogState extends State<_AddChildDialog> { final _formKey = GlobalKey(); final _nameController = TextEditingController(); final _ageController = TextEditingController(); String? _gender; @override void dispose() { _nameController.dispose(); _ageController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Adicionar outra criança'), content: SingleChildScrollView( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 420), 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'; return null; }, ), TextFormField( controller: _ageController, keyboardType: TextInputType.number, textInputAction: TextInputAction.next, 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 < 0 || age > 25) return 'Idade inválida'; 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; }, ), ], ), ), ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(null), child: const Text('Cancelar'), ), FilledButton( onPressed: () { if (!(_formKey.currentState?.validate() ?? false)) return; Navigator.of(context).pop({ 'name': _nameController.text.trim(), 'age': int.parse(_ageController.text.trim()), 'gender': (_gender ?? '').trim(), }); }, child: const Text('Adicionar'), ), ], ); } } 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'; return null; }, ), TextFormField( controller: _ageController, keyboardType: TextInputType.number, textInputAction: TextInputAction.next, 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 < 0 || age > 25) return 'Idade inválida'; 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: SizedBox( height: 44, child: FilledButton( style: FilledButton.styleFrom( backgroundColor: const Color(0xFF2F9E94), foregroundColor: Colors.white, shape: const StadiumBorder(), textStyle: const TextStyle(fontWeight: FontWeight.w900), ), onPressed: _submit, child: const Text('Adicionar'), ), ), ), ], ), ], ), ), ); } }