import 'dart:async'; import 'package:flutter/material.dart'; 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 28 perguntas no total: 5 são sinais de má oclusão já // instalados (queixo, boca/dentição, apinhamento, boca/dentição 2, 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 = 5; 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', ); }), ); } } } void _goToVideo() { Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (_) => QuizVideoGuideScreen( youtubeId: _resultGuideYoutubeId, heading: 'Visualize o seguinte vídeo', caption: 'Assista para compreender a importância de um diagnóstico ' 'precoce e de realizar tratamento ortodôntico intercetivo, ' 'ou seja, uma intervenção realizada na infância, tipicamente ' 'durante a fase de dentição mista (presença tanto de dentes ' 'de leite como de permanentes).\n\nO seu propósito é ' 'identificar, prevenir ou reduzir a gravidade de maloclusões ' 'e problemas no desenvolvimento dos maxilares enquanto os ' 'ossos e dentes ainda estão em crescimento.', onAdvance: (context) => Navigator.of(context).popUntil((r) => r.isFirst), ), ), ); } @override Widget build(BuildContext context) { final recommend = _recommendsEvaluation(widget.finalScore); final signs = widget.finalScore.signs; final factors = widget.finalScore.factors; 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: [ Expanded( child: Center( child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 10), FadeSlideIn( child: const Text( 'O resultado avaliado é de:', 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: 60), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _ResultRing( value: signs, max: kSignsMax, color: const Color(0xFF2F9E94), label: 'Sinais de má\noclusão instalados', ), _ResultRing( value: factors, max: kFactorsMax, color: const Color(0xFFFF55A7), label: 'Fatores de\nrisco associados', ), ], ), ), const SizedBox(height: 22), FadeSlideIn( delay: const Duration(milliseconds: 100), child: _ResultSection( heading: 'Conclusões:', body: 'Foram identificados $signs de $kSignsMax ' 'sinais de má oclusão já instalada e ' '$factors de $kFactorsMax fatores de ' 'risco frequentemente associados a ' 'má oclusão.', bodyColor: const Color(0xFFFF55A7), bodyWeight: FontWeight.w800, ), ), const SizedBox(height: 16), FadeSlideIn( delay: const Duration(milliseconds: 140), child: _ResultSection( heading: 'O que deve fazer agora', body: recommend ? 'Com base neste resultado, recomendamos a marcação de uma consulta com Odontopediatra ou Ortodontista para uma avaliação presencial.' : 'Não foram encontrados sinais ou fatores de risco relevantes. Continue a acompanhar a saúde oral do seu filho/a com as consultas de rotina habituais.', ), ), const SizedBox(height: 16), FadeSlideIn( delay: const Duration(milliseconds: 180), child: _ResultSection( heading: 'Importante saber', body: 'Este resultado não substitui uma avaliação presencial, nem tem função de diagnóstico. O objetivo é ajudá-lo a estar mais atento aos sinais identificados e a não adiar uma consulta que pode fazer a diferença no desenvolvimento oral do seu filho/a.', ), ), ], ), ), ), ), 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; _goToVideo(); }, child: const Text('Avançar'), ), ), ), ), ), ), ], ), ), ), ), ), ], ), ); } } /// Bloco de texto "título + parágrafo" reutilizado nas secções de /// conclusão do resultado ("Conclusões:", "O que deve fazer agora", /// "Importante saber"). class _ResultSection extends StatelessWidget { const _ResultSection({ required this.heading, required this.body, this.bodyColor, this.bodyWeight = FontWeight.w600, }); final String heading; final String body; final Color? bodyColor; final FontWeight bodyWeight; @override Widget build(BuildContext context) { return Column( children: [ Text( heading, textAlign: TextAlign.center, style: TextStyle( color: Colors.black.withValues(alpha: 0.75), fontWeight: FontWeight.w900, ), ), const SizedBox(height: 8), Text( body, textAlign: TextAlign.center, style: TextStyle( color: bodyColor ?? Colors.black.withValues(alpha: 0.70), fontWeight: bodyWeight, height: 1.35, ), ), ], ); } } /// Anel de progresso — usado para os dois indicadores do resultado /// (sinais / fatores de risco), com o valor no centro e a legenda por /// baixo. 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: 130, height: 130, child: Stack( alignment: Alignment.center, children: [ SizedBox( width: 130, height: 130, 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, ), ), ], ); } }