CKT 1.0.2 | otorrinolaringologista

This commit is contained in:
Carlos Correia
2026-07-16 12:58:46 +01:00
parent a8af2c6845
commit f5884d8c36
12 changed files with 611 additions and 53 deletions

View File

@@ -12,6 +12,7 @@ import 'brushing_prefs.dart';
import 'main.dart' show supabase;
import 'quiz/quiz1.dart';
import 'quiz/quiz_prefs.dart';
import 'quiz/quiz_progress_prefs.dart';
import 'quiz/quiz_result.dart' show kSignsMax, kFactorsMax;
import 'screens/settings_screen.dart';
import 'screens/video_screen.dart';
@@ -733,10 +734,91 @@ class _InicioTab extends StatelessWidget {
final state = context.findAncestorStateOfType<_LoggedHomeScreenState>();
state?.selectChild(childName, scopeId);
await Navigator.of(context).push(quizStartRoute(scopeId: scopeId));
final progress = await QuizProgressPrefs.getProgress(scopeId);
if (!context.mounted) return;
Route<void> route;
if (progress != null) {
final resume = await _confirmResumeQuiz(context, childName: childName);
if (!context.mounted) return;
if (resume) {
route = quizResumeRoute(
questionIndex: progress.questionIndex,
score: progress.score,
scopeId: scopeId,
);
} else {
await QuizProgressPrefs.clearProgress();
route = quizStartRoute(scopeId: scopeId);
}
} else {
route = quizStartRoute(scopeId: scopeId);
}
if (!context.mounted) return;
await Navigator.of(context).push(route);
onQuizClosed();
}
/// Pergunta se quer continuar de onde parou ou recomeçar — mostrado só
/// quando há progresso guardado para a mesma criança escolhida agora.
/// Sem opção de cancelar: uma das duas ações inicia sempre o quiz.
Future<bool> _confirmResumeQuiz(
BuildContext context, {
required String childName,
}) async {
final resume = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (ctx) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
title: const Text(
HomeStrings.resumeQuizTitle,
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.w900, color: AppColors.pink),
),
content: Text(
HomeStrings.resumeQuizMessage(childName),
textAlign: TextAlign.center,
style: TextStyle(color: Colors.black.withValues(alpha: 0.72)),
),
actionsAlignment: MainAxisAlignment.center,
actions: [
TapBounce(
child: TextButton(
style: TextButton.styleFrom(foregroundColor: AppColors.teal),
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text(HomeStrings.restartQuiz),
),
),
TapBounce(
child: 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(),
textStyle: const TextStyle(fontWeight: FontWeight.w800),
),
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text(HomeStrings.resumeQuiz),
),
),
),
),
],
);
},
);
return resume ?? true;
}
@override
Widget build(BuildContext context) {
final state = context.findAncestorStateOfType<_LoggedHomeScreenState>();
@@ -795,6 +877,9 @@ class _InicioTab extends StatelessWidget {
onTap: () async {
await Navigator.of(context).push(
MaterialPageRoute<void>(
settings: const RouteSettings(
name: VideoScreen.routeName,
),
builder: (_) => VideoScreen(scopeId: scopeId),
),
);
@@ -1903,6 +1988,14 @@ class _PerfilTabState extends State<_PerfilTab> {
context,
HomeStrings.timeoutAdding,
);
} on PostgrestException catch (e) {
if (!mounted || !context.mounted) return;
showPillSnackBar(
context,
e.code == '23505'
? HomeStrings.childCodeAlreadyInUse
: HomeStrings.errorAdding(e),
);
} catch (e) {
if (!mounted || !context.mounted) return;
showPillSnackBar(context, HomeStrings.errorAdding(e));
@@ -2379,6 +2472,7 @@ class _AddChildSheetState extends State<_AddChildSheet> {
DateTime? _birthDate;
String? _gender;
String? _birthDateError;
String? _genderError;
@override
void dispose() {
@@ -2408,12 +2502,14 @@ class _AddChildSheetState extends State<_AddChildSheet> {
void _submit() {
final formOk = _formKey.currentState?.validate() ?? false;
final genderMissing = (_gender ?? '').trim().isEmpty;
setState(() {
_birthDateError = _birthDate == null
? HomeStrings.birthDateRequired
: null;
_genderError = genderMissing ? HomeStrings.genderRequired : null;
});
if (!formOk || _birthDate == null) return;
if (!formOk || _birthDate == null || genderMissing) return;
Navigator.of(context).pop({
'name': _nameController.text.trim(),
'birth_date': _birthDate!.toIso8601String().split('T').first,
@@ -2514,27 +2610,16 @@ class _AddChildSheetState extends State<_AddChildSheet> {
),
),
),
DropdownButtonFormField<String>(
initialValue: _gender,
items: const [
DropdownMenuItem(
value: HomeStrings.male,
child: Text(HomeStrings.male),
),
DropdownMenuItem(
value: HomeStrings.female,
child: Text(HomeStrings.female),
),
DropdownMenuItem(value: HomeStrings.other, child: Text(HomeStrings.other)),
],
onChanged: (v) => setState(() => _gender = v),
decoration: const InputDecoration(labelText: HomeStrings.gender),
validator: (v) {
if (v == null || v.trim().isEmpty) {
return HomeStrings.genderRequired;
}
return null;
},
Padding(
padding: const EdgeInsets.only(top: 8),
child: _GenderPillSelector(
value: _gender,
errorText: _genderError,
onChanged: (v) => setState(() {
_gender = v;
_genderError = null;
}),
),
),
],
),
@@ -2588,3 +2673,110 @@ class _AddChildSheetState extends State<_AddChildSheet> {
);
}
}
const List<String> _kGenderOptions = [
HomeStrings.male,
HomeStrings.female,
HomeStrings.other,
];
/// Seletor de género em formato de pílulas selecionáveis, no mesmo estilo
/// visual das respostas Sim/Não do quiz — usado em vez de um dropdown
/// genérico.
class _GenderPillSelector extends StatelessWidget {
const _GenderPillSelector({
required this.value,
required this.onChanged,
this.errorText,
});
final String? value;
final ValueChanged<String> onChanged;
final String? errorText;
@override
Widget build(BuildContext context) {
final hasError = errorText != null;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
HomeStrings.gender,
style: TextStyle(
fontSize: 12,
color: hasError
? AppColors.pink
: Colors.black.withValues(alpha: 0.6),
),
),
const SizedBox(height: 8),
Row(
children: [
for (var i = 0; i < _kGenderOptions.length; i++) ...[
if (i > 0) const SizedBox(width: 8),
Expanded(
child: _GenderPill(
label: _kGenderOptions[i],
selected: value == _kGenderOptions[i],
onTap: () => onChanged(_kGenderOptions[i]),
),
),
],
],
),
if (hasError) ...[
const SizedBox(height: 6),
Text(
errorText!,
style: const TextStyle(color: AppColors.pink, fontSize: 12),
),
],
const SizedBox(height: 4),
],
);
}
}
class _GenderPill extends StatelessWidget {
const _GenderPill({
required this.label,
required this.selected,
required this.onTap,
});
final String label;
final bool selected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return TapBounce(
scale: 0.96,
child: InkWell(
borderRadius: BorderRadius.circular(999),
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
alignment: Alignment.center,
decoration: BoxDecoration(
color: selected ? AppColors.teal : Colors.transparent,
borderRadius: BorderRadius.circular(999),
border: Border.all(
color: selected
? AppColors.teal
: Colors.black.withValues(alpha: 0.22),
),
),
child: Text(
label,
style: TextStyle(
fontWeight: FontWeight.w800,
fontSize: 12.5,
color: selected ? Colors.white : Colors.black87,
),
),
),
),
);
}
}