import 'package:flutter/material.dart'; import 'dart:async'; import '../main.dart' show supabase; import '../widgets/app_gradients.dart'; import '../widgets/entrance.dart'; import '../widgets/liquid_waves_background.dart'; import '../widgets/tap_bounce.dart'; import 'quiz_prefs.dart'; import 'quiz_question_screen.dart' show QuizScore; import 'quiz_video_guide.dart'; const String _resultGuideYoutubeId = '3q7C7txH1dE'; // Fase 3 tem 27 perguntas no total: 4 são sinais de má oclusão já // instalados (queixo, boca/dentição, apinhamento, céu da boca) — as // restantes 23 são fatores de risco associados. Ver o diagrama de // planeamento partilhado com o utilizador para o racional completo. const int kSignsMax = 4; const int kFactorsMax = 23; /// A partir de quantos sinais/fatores presentes se recomenda avaliação. /// Proposta: qualquer sinal já instalado, ou ao menos ~30% dos fatores de /// risco — ajustável depois de validar com dados reais. bool _recommendsEvaluation(QuizScore score) { return score.signs >= 1 || score.factors >= (kFactorsMax * 0.3).ceil(); } class QuizResultScreen extends StatefulWidget { const QuizResultScreen({super.key, required this.finalScore, this.scopeId}); final QuizScore finalScore; final String? scopeId; @override State createState() => _QuizResultScreenState(); } class _QuizResultScreenState extends State { late final Future _saveResultFuture; @override void initState() { super.initState(); _saveResultFuture = _saveResult(); } Future _saveResult() async { QuizPrefs.markQuizSeen(); final result = QuizResultData( signs: widget.finalScore.signs, signsMax: kSignsMax, factors: widget.finalScore.factors, factorsMax: kFactorsMax, ); final scope = (widget.scopeId ?? '').trim(); if (scope.isNotEmpty) { await QuizPrefs.saveLastResultForScope(scopeId: scope, result: result); } else { final uid = supabase.auth.currentUser?.id; if (uid != null && uid.trim().isNotEmpty) { await QuizPrefs.saveLastResultForUser(userId: uid, result: result); } else { await QuizPrefs.saveLastResult(result); } } final uid = supabase.auth.currentUser?.id; final userId = (uid ?? '').trim(); if (userId.isNotEmpty && scope.isNotEmpty && scope.startsWith('${userId}_')) { final childId = scope.substring(userId.length + 1).trim(); if (childId.isNotEmpty) { // Fire-and-forget: avoid blocking UI on erros de rede. Requer as // colunas last_signs/last_signs_max/last_factors/last_factors_max // na tabela children — se ainda não existirem na base de dados, // esta chamada falha silenciosamente e o resultado fica só local // (SharedPreferences, acima), tal como já acontecia antes. unawaited( supabase .from('children') .update({ 'last_signs': widget.finalScore.signs, 'last_signs_max': kSignsMax, 'last_factors': widget.finalScore.factors, 'last_factors_max': kFactorsMax, }) .eq('id', childId) .catchError((e) { debugPrint( '[QuizResult] Falha ao gravar score na base de dados: $e', ); }), ); } } } @override Widget build(BuildContext context) { final recommend = _recommendsEvaluation(widget.finalScore); return Scaffold( body: Stack( clipBehavior: Clip.none, children: [ Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))), const LiquidWavesBackground(), SafeArea( child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 520), child: Padding( padding: const EdgeInsets.fromLTRB(22, 12, 22, 18), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Align( alignment: Alignment.centerRight, child: TextButton( onPressed: () => Navigator.of(context).popUntil((r) => r.isFirst), child: const Text(''), ), ), Expanded( child: Center( child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 6), FadeSlideIn( child: const QuizGuideVideoCard( youtubeId: _resultGuideYoutubeId, ), ), const SizedBox(height: 22), FadeSlideIn( delay: const Duration(milliseconds: 60), child: const Text( 'Resultado da avaliação', textAlign: TextAlign.center, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w900, color: Color(0xFFFF55A7), height: 1.2, ), ), ), const SizedBox(height: 18), FadeSlideIn( delay: const Duration(milliseconds: 100), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _ResultRing( value: widget.finalScore.signs, max: kSignsMax, color: const Color(0xFF2F9E94), label: 'Sinais de má\noclusão instalados', ), _ResultRing( value: widget.finalScore.factors, max: kFactorsMax, color: const Color(0xFFFF55A7), label: 'Fatores de\nrisco associados', ), ], ), ), const SizedBox(height: 20), FadeSlideIn( delay: const Duration(milliseconds: 120), child: Text( 'Conclusões:', textAlign: TextAlign.center, style: TextStyle( color: Colors.black.withValues( alpha: 0.75, ), fontWeight: FontWeight.w900, ), ), ), const SizedBox(height: 10), FadeSlideIn( delay: const Duration(milliseconds: 160), child: Text( recommend ? 'Recomenda-se o agendamento de uma consulta com um dentista/ortodontista.\nEsta avaliação é apenas educativa.' : 'Não foram encontrados sinais ou fatores de risco relevantes.\nEsta avaliação é apenas educativa.', textAlign: TextAlign.center, style: TextStyle( color: Colors.black.withValues( alpha: 0.70, ), fontWeight: FontWeight.w600, height: 1.25, ), ), ), ], ), ), ), ), Center( child: TapBounce( child: ClipRRect( borderRadius: BorderRadius.circular(999), child: DecoratedBox( decoration: const BoxDecoration( gradient: kGreenButtonGradient, ), child: SizedBox( width: 260, height: 46, child: FilledButton( style: FilledButton.styleFrom( backgroundColor: Colors.transparent, foregroundColor: Colors.white, shape: const StadiumBorder(), textStyle: const TextStyle( fontWeight: FontWeight.w900, ), ), onPressed: () async { await _saveResultFuture; if (!context.mounted) return; Navigator.of( context, ).popUntil((r) => r.isFirst); }, child: const Text('Avançar'), ), ), ), ), ), ), ], ), ), ), ), ), ], ), ); } } class _ResultRing extends StatelessWidget { const _ResultRing({ required this.value, required this.max, required this.color, required this.label, }); final int value; final int max; final Color color; final String label; @override Widget build(BuildContext context) { final progress = max > 0 ? (value / max).clamp(0.0, 1.0) : 0.0; return Column( mainAxisSize: MainAxisSize.min, children: [ TweenAnimationBuilder( duration: const Duration(milliseconds: 1100), curve: Curves.easeOutCubic, tween: Tween(begin: 0, end: progress), builder: (context, animatedProgress, _) { return SizedBox( width: 150, height: 150, child: Stack( alignment: Alignment.center, children: [ SizedBox( width: 132, height: 132, child: CircularProgressIndicator( value: animatedProgress, strokeWidth: 12, backgroundColor: Colors.black.withValues(alpha: 0.10), valueColor: AlwaysStoppedAnimation(color), ), ), Text( '$value/$max', style: const TextStyle( fontSize: 22, fontWeight: FontWeight.w900, color: Colors.black, ), ), ], ), ); }, ), const SizedBox(height: 10), Text( label, textAlign: TextAlign.center, style: TextStyle( fontSize: 12.5, fontWeight: FontWeight.w800, color: Colors.black.withValues(alpha: 0.65), height: 1.25, ), ), ], ); } }