import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.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 'brushing_prefs.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 'watched_videos_prefs.dart'; import 'widgets/animated_nav_icon.dart'; import 'widgets/app_dialogs.dart'; import 'widgets/app_gradients.dart'; import 'widgets/entrance.dart'; import 'widgets/liquid_waves_background.dart'; import 'widgets/name_input_formatter.dart'; import 'widgets/pill_snackbar.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À-ÖØ-öø-ÿ' -]+$"); /// Calcula a idade a partir de `birth_date` (novo campo). Para crianças /// cadastradas antes desta mudança, que só têm a coluna legada `age`, usa /// esse valor como fallback. int? _childAge(Map child) { final birthDateRaw = (child['birth_date'] ?? '').toString().trim(); if (birthDateRaw.isNotEmpty) { final birthDate = DateTime.tryParse(birthDateRaw); if (birthDate != null) { final now = DateTime.now(); int age = now.year - birthDate.year; if (now.month < birthDate.month || (now.month == birthDate.month && now.day < birthDate.day)) { age--; } return age; } } final legacyAge = child['age']; if (legacyAge is int) return legacyAge; return int.tryParse((legacyAge ?? '').toString()); } 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 = kToolbarHeight; static const double _expandedAppBarHeight = 216; static const double _nameOnlyAppBarHeight = 160; int _index = 0; int _selectedChildIndex = 0; String? _selectedChildName; String? _selectedChildScopeId; QuizResultData? _lastResult; int? _brushingWeekCount; int _weeklyGoal = BrushingPrefs.defaultWeeklyGoal; bool _brushingDailyLimitReached = false; int? _watchedVideoCount; String _cachedUserName = 'Sem nome'; String? _cachedPhotoUrl; @override void initState() { super.initState(); _loadQuizResult(); _loadInitialProfile(); refreshStats(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _maybeStartPendingQuiz(); }); } /// Recarrega os dados locais de escovagem e vídeos assistidos da criança /// atualmente selecionada. Chamado ao trocar de criança, ao editar a meta /// semanal e depois de registar uma escovagem ou um vídeo completo. Future refreshStats() async { final scope = (_selectedChildScopeId ?? '').trim(); if (scope.isEmpty) { if (!mounted) return; setState(() { _brushingWeekCount = null; _weeklyGoal = BrushingPrefs.defaultWeeklyGoal; _brushingDailyLimitReached = false; _watchedVideoCount = null; }); return; } final weekCount = await BrushingPrefs.getWeekCount(scope); final goal = await BrushingPrefs.getWeeklyGoal(scope); final dailyLimitReached = await BrushingPrefs.hasReachedDailyLimit(scope); final watchedCount = await WatchedVideosPrefs.getWatchedCount(scope); if (!mounted) return; setState(() { _brushingWeekCount = weekCount; _weeklyGoal = goal; _brushingDailyLimitReached = dailyLimitReached; _watchedVideoCount = watchedCount; }); } 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(quizStartRoute(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(); await refreshStats(); } catch (_) { // no-op } } Future _loadQuizResult() async { final scope = (_selectedChildScopeId ?? '').trim(); final uid = supabase.auth.currentUser?.id; final String? userId = (uid ?? '').trim().isEmpty ? null : uid; QuizResultData? result; 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 signs = childDoc?['last_signs']; final signsMax = childDoc?['last_signs_max']; final factors = childDoc?['last_factors']; final factorsMax = childDoc?['last_factors_max']; if (signs is int && signsMax is int && factors is int && factorsMax is int) { result = QuizResultData( signs: signs, signsMax: signsMax, factors: factors, factorsMax: factorsMax, ); } } catch (_) { // no-op } } } if (result != null) { if (!mounted) return; setState(() => _lastResult = result); return; } if (scope.isNotEmpty) { result = await QuizPrefs.getLastResultForScope(scope); } else if (userId != null) { result = await QuizPrefs.getLastResultForUser(userId); } else { result = await QuizPrefs.getLastResult(); } if (!mounted) return; setState(() => _lastResult = result); } void selectChild(String? name, String? scopeId) { setState(() { _selectedChildName = name; _selectedChildScopeId = scopeId; }); _loadQuizResult(); refreshStats(); } 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'; } /// Fundo comum (cor sólida + ondas decorativas) atrás de qualquer aba, com /// o conteúdo real posicionado por cima via [SafeArea]/[Padding]. Widget _decoratedBody(Size size, Widget child, double topPadding) { return Stack( clipBehavior: Clip.none, children: [ Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), const LiquidWavesBackground(), SafeArea( top: false, child: Align( alignment: Alignment.center, child: Padding( padding: EdgeInsets.fromLTRB(16, topPadding, 16, 16), child: child, ), ), ), ], ); } Widget _bottomNav() { return BottomNavigationBar( currentIndex: _index, onTap: (i) { if (i == _index) return; HapticFeedback.selectionClick(); setState(() => _index = i); }, backgroundColor: const Color(0xFFFAFAF7), 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', ), ], ); } @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); final QuizResultData? result = _lastResult; final bool hasScore = result != null; final shownName = _cachedUserName; if (_index == 0) { // Home: a app bar é retrátil — encolhe (escondendo o nome da criança e // o gauge) à medida que o utilizador scrolla a lista, mantendo só a // linha do avatar/saudação fixa no topo. É um SliverAppBar dentro de // um NestedScrollView em vez do AppBar fixo usado nas outras abas, // para que a altura acompanhe o scroll em vez de ficar sempre cheia // (o que cortava o conteúdo do card do quiz contra a barra ao rolar). final double expandedHeight = hasScore ? _expandedAppBarHeight : _nameOnlyAppBarHeight; return Scaffold( body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) => [ SliverAppBar( expandedHeight: expandedHeight, toolbarHeight: kToolbarHeight, pinned: true, elevation: 0, scrolledUnderElevation: 0, backgroundColor: _teal, foregroundColor: Colors.white, surfaceTintColor: Colors.transparent, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( bottom: Radius.circular(40), ), ), flexibleSpace: FlexibleSpaceBar( titlePadding: EdgeInsets.zero, background: ClipRRect( borderRadius: const BorderRadius.vertical( bottom: Radius.circular(40), ), child: Container( decoration: const BoxDecoration(gradient: kAppBarGradient), child: Stack( fit: StackFit.expand, children: [ if (hasScore) Positioned( left: 0, right: 0, top: kToolbarHeight + 34, 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: kToolbarHeight, 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, ), ), ), ), //posição da app bar relativamente ao nome if (hasScore) Positioned( left: 0, right: 0, top: kToolbarHeight + 60, child: Center( child: Row( mainAxisSize: MainAxisSize.min, children: [ _RiskArcGauge( value: result.signs, max: result.signsMax, label: 'Sinais', ), const SizedBox(width: 18), _RiskArcGauge( value: result.factors, max: result.factorsMax, label: 'Fatores de risco', ), ], ), ), ), ], ), ), ), ), title: 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, ), ), ], ), ], ), ), ), ), ), ), centerTitle: false, titleSpacing: 0, ), ], body: _decoratedBody( size, _InicioTab(onQuizClosed: _loadQuizResult), 0, ), ), bottomNavigationBar: _bottomNav(), ); } final String title = _index == 1 ? 'Perfil' : 'Configurações'; return Scaffold( appBar: PreferredSize( preferredSize: const Size.fromHeight(_collapsedAppBarHeight), // O gradiente é pintado por este Container de tamanho fixo (a // própria altura da app bar), em vez de confiar no `flexibleSpace` // do AppBar — em alguns aparelhos, o `flexibleSpace` de um AppBar // comum (não Sliver) não recebia o tamanho esperado e a gradiente // aparecia como cor sólida. Com o Container por baixo e o AppBar // totalmente transparente por cima, o gradiente é garantido. child: Container( decoration: const BoxDecoration(gradient: kAppBarGradient), child: AppBar( toolbarHeight: _collapsedAppBarHeight, backgroundColor: Colors.transparent, foregroundColor: Colors.white, surfaceTintColor: Colors.transparent, elevation: 0, scrolledUnderElevation: 0, centerTitle: true, title: Text( title, textAlign: TextAlign.center, style: const TextStyle( fontWeight: FontWeight.w900, color: Colors.white, ), ), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.zero, ), ), ), ), body: _decoratedBody( size, _index == 1 ? _PerfilTab( selectedChildIndex: _selectedChildIndex, onChildSelected: (index, name, scopeId) { setState(() { _selectedChildIndex = index; _selectedChildName = name; _selectedChildScopeId = scopeId; }); _loadQuizResult(); refreshStats(); }, ) : const SettingsBody(), 10, ), bottomNavigationBar: _bottomNav(), ); } } class _RiskArcGauge extends StatelessWidget { const _RiskArcGauge({ required this.value, required this.max, required this.label, }); final int value; final int max; final String label; @override Widget build(BuildContext context) { final progress = max > 0 ? (value / max).clamp(0, 1).toDouble() : 0.0; return TweenAnimationBuilder( duration: const Duration(milliseconds: 700), curve: Curves.easeOutCubic, tween: Tween(begin: 0, end: progress), builder: (context, animatedProgress, _) { return SizedBox( width: 108, height: 84, child: Stack( clipBehavior: Clip.none, children: [ Positioned( top: 0, left: 0, right: 0, height: 60, child: CustomPaint( painter: _RiskArcGaugePainter(progress: animatedProgress), ), ), Positioned( top: 38, left: 0, right: 0, child: Center( child: Text( '$value/$max', style: const TextStyle( color: Colors.white, fontSize: 17, fontWeight: FontWeight.w900, height: 1, ), ), ), ), Positioned( top: 58, left: 0, right: 0, child: Text( label, textAlign: TextAlign.center, style: TextStyle( color: Colors.white.withValues(alpha: 0.85), fontSize: 10.5, fontWeight: FontWeight.w700, height: 1.15, letterSpacing: 0.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.round; final progressPaint = Paint() ..color = const Color(0xFFFF9AD0) ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth ..strokeCap = StrokeCap.round; 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(quizStartRoute(scopeId: scopeId)); onQuizClosed(); } @override Widget build(BuildContext context) { final state = context.findAncestorStateOfType<_LoggedHomeScreenState>(); final selectedChildName = (state?._selectedChildName ?? '').trim(); final scopeId = state?._selectedChildScopeId; 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: [ if (selectedChildName.isNotEmpty) ...[ FadeSlideIn( child: _HomeSectionLabel('Para $selectedChildName'), ), const SizedBox(height: 8), ], FadeSlideIn( child: TapBounce( scale: 0.97, child: _HeroQuizCard( onStartQuiz: () => _startQuiz(context), ), ), ), const SizedBox(height: 16), FadeSlideIn( delay: const Duration(milliseconds: 70), child: _StatsRow( brushingCount: state?._brushingWeekCount, weeklyGoal: state?._weeklyGoal ?? BrushingPrefs.defaultWeeklyGoal, brushedToday: state?._brushingDailyLimitReached ?? false, watchedCount: state?._watchedVideoCount, onTapBrushing: () => _logBrushing(context, state, scopeId), ), ), const SizedBox(height: 20), FadeSlideIn( delay: const Duration(milliseconds: 110), child: const _HomeSectionLabel('Vídeos educativos'), ), const SizedBox(height: 8), FadeSlideIn( delay: const Duration(milliseconds: 130), child: TapBounce( scale: 0.97, child: _VideoLibraryCard( watchedCount: state?._watchedVideoCount ?? 0, onTap: () async { await Navigator.of(context).push( MaterialPageRoute( builder: (_) => VideoScreen(scopeId: scopeId), ), ); await state?.refreshStats(); }, ), ), ), const SizedBox(height: 16), ], ), ), ), ), ); } Future _logBrushing( BuildContext context, _LoggedHomeScreenState? state, String? scopeId, ) async { final scope = (scopeId ?? '').trim(); if (scope.isEmpty) { final uid = (supabase.auth.currentUser?.id ?? '').trim(); if (uid.isEmpty) return; await _requireFirstChild(context, uid); return; } if (!(await BrushingPrefs.canLogMore(scope))) { if (context.mounted) { showPillSnackBar( context, 'Já registou as ${BrushingPrefs.maxPerDay} escovagens de hoje!', ); } return; } await BrushingPrefs.logToday(scope); await state?.refreshStats(); if (context.mounted) { showPillSnackBar(context, 'Escovagem registada!'); } } } /// Faz [child] "respirar" (escala sobe e desce suavemente, em loop) — usado /// para chamar a atenção para os dois destaques principais da Home (o quiz /// e o vídeo em destaque), que são o carro-chefe da app. class _Pulse extends StatefulWidget { const _Pulse({ required this.child, this.minScale = 0.94, this.maxScale = 1.0, this.duration = const Duration(milliseconds: 1100), }); final Widget child; final double minScale; final double maxScale; final Duration duration; @override State<_Pulse> createState() => _PulseState(); } class _PulseState extends State<_Pulse> with SingleTickerProviderStateMixin { late final AnimationController _controller = AnimationController( vsync: this, duration: widget.duration, )..repeat(reverse: true); late final Animation _scale = Tween( begin: widget.minScale, end: widget.maxScale, ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return ScaleTransition(scale: _scale, child: widget.child); } } /// Pequeno rótulo maiúsculo discreto usado para separar as secções da Home /// ("Para {nome}", "Novo episódio", "Continuar a aprender"). class _HomeSectionLabel extends StatelessWidget { const _HomeSectionLabel(this.text); final String text; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(left: 4), child: Text( text.toUpperCase(), style: TextStyle( fontSize: 12, fontWeight: FontWeight.w800, letterSpacing: 0.6, color: Colors.black.withValues(alpha: 0.45), ), ), ); } } class _StatsRow extends StatelessWidget { const _StatsRow({ required this.brushingCount, required this.weeklyGoal, required this.brushedToday, required this.watchedCount, required this.onTapBrushing, }); final int? brushingCount; final int weeklyGoal; final bool brushedToday; final int? watchedCount; final VoidCallback onTapBrushing; @override Widget build(BuildContext context) { return IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( child: TapBounce( scale: 0.97, child: _StatCard( icon: Icons.brush_rounded, iconColor: const Color(0xFFFF55A7), value: brushingCount == null ? '--' : '$brushingCount/$weeklyGoal', label: 'Escovagens esta semana', done: brushedToday, onTap: onTapBrushing, ), ), ), const SizedBox(width: 12), Expanded( child: _StatCard( icon: Icons.movie_filter_rounded, iconColor: const Color(0xFF8E7CC3), value: watchedCount == null ? '--' : '$watchedCount', label: 'Episódios completos', ), ), ], ), ); } } class _StatCard extends StatelessWidget { const _StatCard({ required this.icon, required this.iconColor, required this.value, required this.label, this.onTap, this.done = false, }); final IconData icon; final Color iconColor; final String value; final String label; final VoidCallback? onTap; final bool done; @override Widget build(BuildContext context) { return Material( color: Colors.white, borderRadius: BorderRadius.circular(20), elevation: 8, shadowColor: Colors.black.withValues(alpha: 0.10), child: InkWell( borderRadius: BorderRadius.circular(20), onTap: onTap, child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Stack( clipBehavior: Clip.none, children: [ Container( width: 38, height: 38, decoration: BoxDecoration( color: iconColor.withValues(alpha: 0.14), borderRadius: BorderRadius.circular(12), ), child: Icon(icon, color: iconColor, size: 20), ), if (done) Positioned( top: -4, right: -4, child: Container( width: 16, height: 16, decoration: const BoxDecoration( color: Color(0xFF2F9E94), shape: BoxShape.circle, ), child: const Icon( Icons.check_rounded, size: 11, color: Colors.white, ), ), ), ], ), const SizedBox(height: 10), Text( value, style: const TextStyle( fontWeight: FontWeight.w900, fontSize: 20, ), ), const SizedBox(height: 2), Text( label, style: TextStyle( fontSize: 11.5, fontWeight: FontWeight.w600, color: Colors.black.withValues(alpha: 0.6), ), ), ], ), ), ), ); } } 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; showPillSnackBar(context, '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 = _childAge(c); 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.onStartQuiz}); final VoidCallback onStartQuiz; @override Widget build(BuildContext context) { return Material( elevation: 18, shadowColor: const Color(0xFFFF55A7).withValues(alpha: 0.45), borderRadius: BorderRadius.circular(28), clipBehavior: Clip.antiAlias, color: Colors.transparent, child: Ink( decoration: const BoxDecoration(gradient: kPinkHeroGradient), child: Stack( clipBehavior: Clip.none, children: [ Positioned( right: -34, bottom: -34, child: IgnorePointer( child: Opacity( opacity: 0.14, child: Container( width: 160, height: 160, decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), ), ), ), ), Positioned( right: 4, bottom: 10, child: IgnorePointer( child: Opacity( opacity: 0.16, child: Transform.rotate( angle: -0.22, child: const Icon( Icons.health_and_safety_rounded, size: 92, color: Colors.white, ), ), ), ), ), Positioned( left: -18, top: -18, child: IgnorePointer( child: Opacity( opacity: 0.10, child: Container( width: 70, height: 70, decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), ), ), ), ), Padding( padding: const EdgeInsets.fromLTRB(20, 20, 20, 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 5, ), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.22), borderRadius: BorderRadius.circular(999), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.bolt_rounded, color: Colors.white, size: 14, ), SizedBox(width: 4), Text( 'Avaliação gratuita', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w800, fontSize: 12, ), ), ], ), ), const SizedBox(width: 8), _Pulse( minScale: 0.85, maxScale: 1.15, duration: const Duration(milliseconds: 900), child: Container( width: 8, height: 8, decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), ), ), ], ), const SizedBox(height: 14), const Text( 'Avaliação de saúde oral', style: TextStyle( fontWeight: FontWeight.w900, fontSize: 22, height: 1.1, color: Colors.white, ), ), const SizedBox(height: 5), Text( '27 perguntas rápidas · menos de 3 minutos', style: TextStyle( color: Colors.white.withValues(alpha: 0.92), fontWeight: FontWeight.w600, fontSize: 13, ), ), const SizedBox(height: 18), _Pulse( minScale: 0.985, maxScale: 1.0, duration: const Duration(milliseconds: 1400), child: SizedBox( height: 50, width: double.infinity, child: FilledButton.icon( style: FilledButton.styleFrom( backgroundColor: Colors.white, foregroundColor: const Color(0xFFFF55A7), shape: const StadiumBorder(), elevation: 6, shadowColor: Colors.black.withValues(alpha: 0.25), textStyle: const TextStyle( fontWeight: FontWeight.w800, fontSize: 15.5, ), ), onPressed: onStartQuiz, icon: const Icon(Icons.play_arrow_rounded), label: const Text('Iniciar Quiz'), ), ), ), ], ), ), ], ), ), ); } } /// Card de destaque (mesma linguagem visual do card do quiz: gradiente, /// badge, título, botão branco) que convida a criança/pai a ir ver a /// biblioteca de vídeos — sem nenhuma miniatura/imagem de vídeo específica, /// só ícone e texto. Um único toque (no card ou no botão) leva direto à /// grelha onde se escolhe qual episódio assistir. class _VideoLibraryCard extends StatelessWidget { const _VideoLibraryCard({required this.watchedCount, required this.onTap}); final int watchedCount; final VoidCallback onTap; @override Widget build(BuildContext context) { return Material( elevation: 14, shadowColor: const Color(0xFF2F9E94).withValues(alpha: 0.38), borderRadius: BorderRadius.circular(28), clipBehavior: Clip.antiAlias, color: Colors.transparent, child: Ink( decoration: const BoxDecoration(gradient: kGreenButtonGradient), child: InkWell( onTap: onTap, child: Stack( clipBehavior: Clip.none, children: [ Positioned( right: -30, bottom: -30, child: IgnorePointer( child: Opacity( opacity: 0.14, child: Container( width: 150, height: 150, decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), ), ), ), ), Positioned( right: 6, bottom: 6, child: IgnorePointer( child: Opacity( opacity: 0.16, child: Transform.rotate( angle: 0.2, child: const Icon( Icons.smart_display_rounded, size: 88, color: Colors.white, ), ), ), ), ), Padding( padding: const EdgeInsets.fromLTRB(20, 20, 20, 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 5, ), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.22), borderRadius: BorderRadius.circular(999), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.play_circle_fill_rounded, color: Colors.white, size: 14, ), SizedBox(width: 4), Text( 'Biblioteca de vídeos', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w800, fontSize: 12, ), ), ], ), ), const SizedBox(height: 14), const Text( 'Vídeos educativos', style: TextStyle( fontWeight: FontWeight.w900, fontSize: 22, height: 1.1, color: Colors.white, ), ), const SizedBox(height: 5), Text( watchedCount > 0 ? '$watchedCount episódio${watchedCount == 1 ? '' : 's'} completo${watchedCount == 1 ? '' : 's'} · ${videoList.length} no total' : '${videoList.length} episódios sobre saúde oral para toda a família', style: TextStyle( color: Colors.white.withValues(alpha: 0.92), fontWeight: FontWeight.w600, fontSize: 13, ), ), const SizedBox(height: 18), SizedBox( height: 48, width: double.infinity, child: FilledButton.icon( style: FilledButton.styleFrom( backgroundColor: Colors.white, foregroundColor: const Color(0xFF2F9E94), shape: const StadiumBorder(), textStyle: const TextStyle( fontWeight: FontWeight.w800, fontSize: 15, ), ), onPressed: onTap, icon: const Icon(Icons.video_library_rounded), label: const Text('Ver vídeos'), ), ), ], ), ), ], ), ), ), ); } } 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 _loadScoreForScope(String scopeId) { return QuizPrefs.getLastResultForScope(scopeId); } 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; showPillSnackBar(context, 'Erro ao enviar foto: $e'); } finally { if (mounted) setState(() => _updatingPhoto = false); } } Future _confirmDeleteChild( BuildContext context, { required String childId, required String childName, }) async { 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(); if (context.mounted) showPillSnackBar(context, 'Criança removida'); } catch (e) { if (context.mounted) { showPillSnackBar(context, 'Erro ao remover: $e'); } } } Future _editWeeklyGoal( BuildContext context, { required String scopeId, required String childName, }) async { final current = await BrushingPrefs.getWeeklyGoal(scopeId); if (!context.mounted) return; final controller = TextEditingController(text: current.toString()); final saved = await showDialog( context: context, builder: (ctx) { return AlertDialog( backgroundColor: const Color(0xFFFFE6F1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), title: Text( 'Meta semanal de $childName', style: const TextStyle( fontWeight: FontWeight.w900, color: Color(0xFFFF55A7), ), ), content: TextField( controller: controller, keyboardType: TextInputType.number, autofocus: true, decoration: const InputDecoration( labelText: 'Escovagens por semana', helperText: 'Entre 1 e 21 (até 3 por dia)', ), ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('Cancelar'), ), ClipRRect( borderRadius: BorderRadius.circular(999), child: DecoratedBox( decoration: const BoxDecoration(gradient: kGreenButtonGradient), child: FilledButton( style: FilledButton.styleFrom( backgroundColor: Colors.transparent, foregroundColor: Colors.white, shape: const StadiumBorder(), ), onPressed: () { final value = int.tryParse(controller.text.trim()); if (value == null || value < 1 || value > 21) return; Navigator.of(ctx).pop(value); }, child: const Text('Guardar'), ), ), ), ], ); }, ); if (saved == null) return; await BrushingPrefs.setWeeklyGoal(scopeId, saved); if (!context.mounted) return; context.findAncestorStateOfType<_LoggedHomeScreenState>()?.refreshStats(); setState(() {}); } Future _addAnotherChild(BuildContext context, String uid) async { if (_addingChild) return; 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(); if (context.mounted) showPillSnackBar(context, '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 || !context.mounted) return; showPillSnackBar( context, 'Tempo esgotado ao adicionar. Tente novamente.', ); } catch (e) { if (!mounted || !context.mounted) return; showPillSnackBar(context, '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 = _childAge(c); 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( future: _loadScoreForScope(scopeId), builder: (context, snap) { final result = snap.data; final text = result == null ? '--' : '${result.signs}/${result.signsMax} · ${result.factors}/${result.factorsMax}'; 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), ), ), ); }, ), IconButton( onPressed: () => _editWeeklyGoal( context, scopeId: scopeId, childName: title, ), icon: const Icon( Icons.edit_outlined, color: Color(0xFF2F9E94), ), tooltip: 'Meta semanal de escovagens', visualDensity: VisualDensity.compact, ), 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(); DateTime? _birthDate; String? _gender; String? _birthDateError; @override void dispose() { _nameController.dispose(); super.dispose(); } Future _pickBirthDate() async { final now = DateTime.now(); final picked = await showDatePicker( context: context, initialDate: _birthDate ?? DateTime(now.year - 5, now.month, now.day), firstDate: DateTime(now.year - 17, now.month, now.day), lastDate: DateTime(now.year - 1, now.month, now.day), helpText: 'Data de nascimento', cancelText: 'Cancelar', confirmText: 'Confirmar', locale: const Locale('pt', 'PT'), ); if (picked == null) return; setState(() { _birthDate = picked; _birthDateError = null; }); } void _submit() { final formOk = _formKey.currentState?.validate() ?? false; setState(() { _birthDateError = _birthDate == null ? 'Informe a data de nascimento' : null; }); if (!formOk || _birthDate == null) return; Navigator.of(context).pop({ 'name': _nameController.text.trim(), 'birth_date': _birthDate!.toIso8601String().split('T').first, '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, textCapitalization: TextCapitalization.sentences, inputFormatters: [CapitalizeFirstLetterFormatter()], 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; }, ), InkWell( borderRadius: BorderRadius.circular(8), onTap: _pickBirthDate, child: InputDecorator( decoration: InputDecoration( labelText: 'Data de nascimento', errorText: _birthDateError, suffixIcon: const Icon( Icons.calendar_today_rounded, size: 18, ), ), child: Text( _birthDate == null ? 'Selecione a data' : '${_birthDate!.day.toString().padLeft(2, '0')}/' '${_birthDate!.month.toString().padLeft(2, '0')}/' '${_birthDate!.year}', style: _birthDate == null ? TextStyle( color: Colors.black.withValues(alpha: 0.4), ) : 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'), ), ), ), ), ), ), ], ), ], ), ), ); } }