CKT 1.0.2 | otorrinolaringologista
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user