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/curiosidade_screen.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 ? '' : _index == 1 ? 'Perfil' : 'Configurações'; 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: 24, child: Center( child: TweenAnimationBuilder( duration: const Duration(milliseconds: 520), curve: Curves.easeOutCubic, tween: Tween( begin: 0, end: percent / 100.0, ), builder: (context, value, _) { final shown = (value * 100).round(); return Container( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 8, ), decoration: BoxDecoration( color: Colors.white.withValues( alpha: 0.18, ), borderRadius: BorderRadius.circular(999), border: Border.all( color: Colors.white.withValues( alpha: 0.22, ), ), ), child: Text( '$shown%', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w900, ), ), ); }, ), ), ), ], ), 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) : _index == 1 ? _PerfilTab( selectedChildIndex: _selectedChildIndex, onChildSelected: (index, name, scopeId) { setState(() { _selectedChildIndex = index; _selectedChildName = name; _selectedChildScopeId = scopeId; }); _loadQuizResult(); }, ) : const _ConfigTab(), ), ), ), ], ), 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', ), BottomNavigationBarItem( icon: Icon(Icons.settings_rounded), label: 'Config.', ), ], ), ); } } 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); return Align( alignment: Alignment.topCenter, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 560), child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.only(top: 18, bottom: 10), child: Column( mainAxisSize: MainAxisSize.min, children: [ _MenuButton( label: 'Iniciar Quiz', onPressed: () { Navigator.of(context) .push( MaterialPageRoute( builder: (_) => Quiz1Screen(scopeId: scopeId), ), ) .then((_) => onQuizClosed()); }, ), const SizedBox(height: 14), _VideoLibraryCard( onOpenLibrary: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => const VideoScreen(), ), ); }, ), const SizedBox(height: 14), _MenuButton( label: 'Curiosidades', onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => const CuriosidadeScreen(), ), ); }, ), const SizedBox(height: 14), const _ClinicsSection(), ], ), ), ), ), ); } } class ClinicItem { const ClinicItem({ required this.name, required this.subtitle, required this.rating, required this.description, required this.address, required this.phone, }); final String name; final String subtitle; final double rating; final String description; final String address; final String phone; } class _ClinicsSection extends StatelessWidget { const _ClinicsSection(); static const List _clinics = [ ClinicItem( name: 'PóvaMed', subtitle: 'Clínica Médica e Dentária', rating: 4.9, description: 'Atendimento odontopediátrico e familiar. Agende e tire dúvidas pelo telefone.', address: 'R. Patrão Lagoa 12, 4490-578 Póvoa de Varzim', phone: '+351 000 000 000', ), ClinicItem( name: 'ORTO-M', subtitle: 'Ortodontia e Implantes', rating: 3.8, description: 'Avaliação inicial e planos de ortodontia. Atendimento por marcação.', address: 'R. de 31 de Janeiro, 4490-533 Póvoa de Varzim', phone: '+351 000 000 001', ), ClinicItem( name: 'Dentart', subtitle: 'Clínica Médica E Dentária', rating: 5.0, description: 'Avaliação inicial e planos de ortodontia. Atendimento por marcação.', address: 'Rua Ramalho Ortigão 198 R/C, 4490-678 Póvoa De Varzim', phone: '+351 000 000 001', ), ClinicItem( name: 'S. Cipriano', subtitle: 'Clínica Médico-Cirúrgica', rating: 3.0, description: 'Avaliação inicial e planos de ortodontia. Atendimento por marcação.', address: 'Praça do Almada 7 2º, 4490-438 Póvoa De Varzim', phone: '+351 000 000 001', ), ]; @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); return LayoutBuilder( builder: (context, constraints) { final double cardWidth = math.min( constraints.maxWidth, size.width * 0.78, ); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Padding( padding: EdgeInsets.only(bottom: 8, top: 8), child: Text( 'Clínicas Parceiras', style: TextStyle( color: Color(0xFF2F9E94), fontWeight: FontWeight.w900, fontSize: 16, ), ), ), Container( height: 2.6, decoration: BoxDecoration( color: const Color(0xFF2F9E94).withValues(alpha: 0.85), borderRadius: BorderRadius.circular(999), ), ), const SizedBox(height: 12), for (final clinic in _clinics) ...[ Center( child: SizedBox( width: cardWidth, child: _ClinicCard(item: clinic), ), ), const SizedBox(height: 12), ], ], ); }, ); } } class _ClinicCard extends StatelessWidget { const _ClinicCard({required this.item}); final ClinicItem item; @override Widget build(BuildContext context) { return Material( color: const Color(0xFFFF55A7), borderRadius: BorderRadius.circular(22), elevation: 10, shadowColor: Colors.black.withValues(alpha: 0.18), child: InkWell( borderRadius: BorderRadius.circular(22), onTap: () { 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: [ Text( item.name, textAlign: TextAlign.center, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w900, color: Color(0xFFFF55A7), ), ), const SizedBox(height: 6), Text( item.subtitle, textAlign: TextAlign.center, style: TextStyle( color: Colors.black.withValues(alpha: 0.72), fontWeight: FontWeight.w800, ), ), 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: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _ClinicInfoRow( label: 'Avaliação', value: item.rating.toStringAsFixed(1), ), const SizedBox(height: 10), _ClinicInfoRow( label: 'Endereço', value: item.address, ), const SizedBox(height: 10), _ClinicInfoRow(label: 'Contato', value: item.phone), const SizedBox(height: 12), Text( item.description, style: TextStyle( color: Colors.black.withValues(alpha: 0.72), fontWeight: FontWeight.w600, height: 1.25, ), ), ], ), ), ], ), ), ); }, ); }, child: Padding( padding: const EdgeInsets.fromLTRB(16, 14, 16, 16), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w900, fontSize: 16, ), ), const SizedBox(height: 2), Text( item.subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: Colors.white.withValues(alpha: 0.90), fontWeight: FontWeight.w700, fontSize: 12, ), ), const SizedBox(height: 6), Text( item.address, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: Colors.white.withValues(alpha: 0.88), fontWeight: FontWeight.w700, fontSize: 11, ), ), ], ), ), const SizedBox(width: 10), Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.18), borderRadius: BorderRadius.circular(999), border: Border.all( color: Colors.white.withValues(alpha: 0.22), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.star_rounded, color: Colors.white, size: 18, ), const SizedBox(width: 4), Text( item.rating.toStringAsFixed(1), style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w900, ), ), ], ), ), ], ), ), ), ); } } class _ClinicInfoRow extends StatelessWidget { const _ClinicInfoRow({required this.label, required this.value}); final String label; final String value; @override Widget build(BuildContext context) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 86, child: Text( label, style: const TextStyle( color: Color(0xFF2F9E94), fontWeight: FontWeight.w900, ), ), ), const SizedBox(width: 10), Expanded( child: Text( value, style: TextStyle( color: Colors.black.withValues(alpha: 0.74), fontWeight: FontWeight.w700, ), ), ), ], ); } } class _VideoLibraryCard extends StatelessWidget { const _VideoLibraryCard({required this.onOpenLibrary}); final VoidCallback onOpenLibrary; @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); final item = VideoScreen.library.isEmpty ? null : VideoScreen.library.first; return SizedBox( width: size.width * 0.78, child: Material( color: const Color(0xFFFF55A7), borderRadius: BorderRadius.circular(22), elevation: 10, shadowColor: Colors.black.withValues(alpha: 0.18), child: InkWell( borderRadius: BorderRadius.circular(22), onTap: onOpenLibrary, child: Padding( padding: const EdgeInsets.fromLTRB(16, 14, 16, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ const Expanded( child: Text( 'Vídeos Educativos', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w900, fontSize: 16, ), ), ), Icon( Icons.chevron_right_rounded, color: Colors.white.withValues(alpha: 0.95), size: 26, ), ], ), const SizedBox(height: 14), if (item == null) Container( height: 140, decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.18), borderRadius: BorderRadius.circular(18), ), ) else _VideoLargePreview( title: item.title, url: item.url, onTap: onOpenLibrary, ), ], ), ), ), ), ); } } class _VideoLargePreview extends StatelessWidget { const _VideoLargePreview({ required this.title, required this.url, required this.onTap, }); final String title; final String url; final VoidCallback onTap; @override Widget build(BuildContext context) { return Material( color: Colors.white.withValues(alpha: 0.22), borderRadius: BorderRadius.circular(18), child: InkWell( borderRadius: BorderRadius.circular(18), onTap: onTap, child: SizedBox( height: 140, child: ClipRRect( borderRadius: BorderRadius.circular(18), child: Stack( fit: StackFit.expand, children: [ _VideoThumbnail(url: url), Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withValues(alpha: 0.10), Colors.black.withValues(alpha: 0.58), ], ), ), ), const Align( alignment: Alignment.center, child: Icon( Icons.play_circle_fill_rounded, size: 58, color: Colors.white, ), ), Align( alignment: Alignment.bottomLeft, child: Padding( padding: const EdgeInsets.fromLTRB(14, 10, 14, 14), child: Text( title, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w900, fontSize: 14, ), ), ), ), ], ), ), ), ), ); } } 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 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(); 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), child: Padding( padding: const EdgeInsets.all(16), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ InkWell( borderRadius: BorderRadius.circular(16), onTap: _updatingPhoto ? null : () => _pickAndUploadProfilePhoto( context, uid, ), child: Container( width: 72, height: 72, decoration: BoxDecoration( color: const Color(0xFFFFE6F1), borderRadius: BorderRadius.circular(16), ), 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: 40, 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, ), ), ), ), ], ), ), ), const SizedBox(width: 12), Expanded( child: Text( profileName, style: const TextStyle( fontSize: 22, fontWeight: FontWeight.w900, color: Color(0xFFFF55A7), ), ), ), ], ), ), ), const SizedBox(height: 14), if (children.isEmpty) const SizedBox.shrink() 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: 46, child: FilledButton( 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), child: const Text('Adicionar outra criança'), ), ), ], ), ), ), ), ); }, ); }, ); } } 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'), ), ), ), ], ), ], ), ), ); } } class _ConfigTab extends StatelessWidget { const _ConfigTab(); @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); final Color accentPink = const Color(0xFFFF55A7); return Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: size.width * 0.60, height: 44, child: FilledButton( style: FilledButton.styleFrom( backgroundColor: accentPink, foregroundColor: Colors.white, shape: const StadiumBorder(), textStyle: const TextStyle( fontWeight: FontWeight.w800, fontSize: 15, ), ).copyWith( animationDuration: const Duration(milliseconds: 180), splashFactory: InkSparkle.splashFactory, overlayColor: WidgetStateProperty.resolveWith(( states, ) { if (states.contains(WidgetState.pressed)) { return Colors.white.withValues(alpha: 0.14); } if (states.contains(WidgetState.hovered) || states.contains(WidgetState.focused)) { return Colors.white.withValues(alpha: 0.08); } return null; }), ), onPressed: () async { await FirebaseAuth.instance.signOut(); }, child: const Text('Sair'), ), ), ], ); } } class _MenuButton extends StatelessWidget { const _MenuButton({required this.label, required this.onPressed}); final String label; final VoidCallback onPressed; @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); const Color accentPink = Color(0xFFFF55A7); return SizedBox( width: size.width * 0.78, height: 50, child: FilledButton( style: FilledButton.styleFrom( backgroundColor: accentPink, foregroundColor: Colors.white, shape: const StadiumBorder(), textStyle: const TextStyle( fontWeight: FontWeight.w800, fontSize: 15, ), ).copyWith( animationDuration: const Duration(milliseconds: 180), splashFactory: InkSparkle.splashFactory, overlayColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.pressed)) { return Colors.white.withValues(alpha: 0.14); } if (states.contains(WidgetState.hovered) || states.contains(WidgetState.focused)) { return Colors.white.withValues(alpha: 0.08); } return null; }), ), onPressed: onPressed, child: Text(label, textAlign: TextAlign.center), ), ); } }