app/lib/screens/baseline_screen.dart
John Mizerek 498a1534ed Add prompt to continue from most recent saved run
When tapping Start with existing saved runs, shows dialog asking
whether to continue from last run or start fresh. Continuing pre-fills
all profile and behavior inputs from the saved run.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 20:49:37 -08:00

493 lines
14 KiB
Dart

import 'package:flutter/material.dart';
import '../models/models.dart';
import '../risk_engine/mortality_tables.dart';
import '../storage/local_storage.dart';
import '../theme.dart';
import 'about_screen.dart';
import 'behavioral_screen.dart';
class BaselineScreen extends StatefulWidget {
final bool readOnly;
final UserProfile? initialProfile;
final BehavioralInputs? initialBehaviors;
const BaselineScreen({
super.key,
this.readOnly = false,
this.initialProfile,
this.initialBehaviors,
});
@override
State<BaselineScreen> createState() => _BaselineScreenState();
}
class _BaselineScreenState extends State<BaselineScreen> {
int _age = 35;
Sex _sex = Sex.male;
String _country = 'United States';
double _heightCm = 170;
double _weightKg = 70;
final Set<Diagnosis> _diagnoses = {};
bool _useMetric = false;
late List<String> _countries;
@override
void initState() {
super.initState();
_countries = getSupportedCountries();
_loadInitialData();
}
Future<void> _loadInitialData() async {
// Load unit preference
final useMetric = await LocalStorage.getUseMetricUnits();
setState(() => _useMetric = useMetric);
// If initial profile provided, use that
if (widget.initialProfile != null) {
_applyProfile(widget.initialProfile!);
return;
}
// Otherwise load from storage
final profile = await LocalStorage.getProfile();
if (profile != null) {
_applyProfile(profile);
}
}
void _applyProfile(UserProfile profile) {
setState(() {
_age = profile.age;
_sex = profile.sex;
_country = profile.country;
_heightCm = profile.heightCm;
_weightKg = profile.weightKg;
_diagnoses.clear();
_diagnoses.addAll(profile.diagnoses);
});
}
double get _bmi => _weightKg / ((_heightCm / 100) * (_heightCm / 100));
String get _bmiCategory {
if (_bmi < 18.5) return 'Underweight';
if (_bmi < 25) return 'Normal';
if (_bmi < 30) return 'Overweight';
if (_bmi < 35) return 'Obese I';
if (_bmi < 40) return 'Obese II';
return 'Obese III';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.readOnly ? 'Baseline (View Only)' : 'Baseline'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
actions: [
IconButton(
icon: Icon(_useMetric ? Icons.straighten : Icons.square_foot),
tooltip: _useMetric ? 'Using Metric' : 'Using Imperial',
onPressed: widget.readOnly ? null : _toggleUnits,
),
IconButton(
icon: const Icon(Icons.info_outline),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const AboutScreen()),
),
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Demographics',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(
'This information establishes your baseline life expectancy.',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 24),
// Age
_buildSectionLabel('Age'),
const SizedBox(height: 8),
_buildAgeSelector(),
const SizedBox(height: 24),
// Sex
_buildSectionLabel('Biological Sex'),
const SizedBox(height: 8),
_buildSexSelector(),
const SizedBox(height: 24),
// Country
_buildSectionLabel('Country'),
const SizedBox(height: 8),
_buildCountryDropdown(),
const SizedBox(height: 24),
// Height
_buildSectionLabel('Height'),
const SizedBox(height: 8),
_buildHeightSlider(),
const SizedBox(height: 24),
// Weight
_buildSectionLabel('Weight'),
const SizedBox(height: 8),
_buildWeightSlider(),
const SizedBox(height: 16),
// BMI display
_buildBmiDisplay(),
const SizedBox(height: 32),
// Existing conditions
Text(
'Existing Conditions',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(
'Select any diagnosed conditions. These affect baseline calculations but are not modifiable factors.',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
_buildDiagnosisCheckboxes(),
const SizedBox(height: 32),
// Continue button (hidden in readOnly mode)
if (!widget.readOnly)
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _continue,
child: const Text('Continue'),
),
),
const SizedBox(height: 16),
],
),
),
);
}
Widget _buildSectionLabel(String label) {
return Text(
label,
style: Theme.of(context).textTheme.labelLarge,
);
}
Widget _buildAgeSelector() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: AppColors.surfaceVariant,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: widget.readOnly || _age <= 18
? null
: () => setState(() => _age--),
),
Expanded(
child: Text(
'$_age years',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineMedium,
),
),
IconButton(
icon: const Icon(Icons.add),
onPressed: widget.readOnly || _age >= 100
? null
: () => setState(() => _age++),
),
],
),
);
}
Widget _buildSexSelector() {
return Row(
children: [
Expanded(
child: _buildToggleButton(
'Male',
_sex == Sex.male,
widget.readOnly ? null : () => setState(() => _sex = Sex.male),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildToggleButton(
'Female',
_sex == Sex.female,
widget.readOnly ? null : () => setState(() => _sex = Sex.female),
),
),
],
);
}
Widget _buildToggleButton(String label, bool selected, VoidCallback? onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: selected ? AppColors.primary : AppColors.surfaceVariant,
borderRadius: BorderRadius.circular(12),
),
child: Text(
label,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: selected ? Colors.white : AppColors.textSecondary,
),
),
),
);
}
Widget _buildCountryDropdown() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: AppColors.surfaceVariant,
borderRadius: BorderRadius.circular(12),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _country,
isExpanded: true,
icon: const Icon(Icons.keyboard_arrow_down),
items: _countries.map((country) {
return DropdownMenuItem(
value: country,
child: Text(country),
);
}).toList(),
onChanged: widget.readOnly
? null
: (value) {
if (value != null) setState(() => _country = value);
},
),
),
);
}
Widget _buildHeightSlider() {
final primaryText = _useMetric
? '${_heightCm.round()} cm'
: _cmToFeetInches(_heightCm);
final secondaryText = _useMetric
? _cmToFeetInches(_heightCm)
: '${_heightCm.round()} cm';
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
primaryText,
style: Theme.of(context).textTheme.headlineMedium,
),
Text(
secondaryText,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
Slider(
value: _heightCm,
min: 120,
max: 220,
divisions: 100,
onChanged: widget.readOnly
? null
: (value) => setState(() => _heightCm = value),
),
],
);
}
Widget _buildWeightSlider() {
final lbs = (_weightKg * 2.205).round();
final primaryText = _useMetric ? '${_weightKg.round()} kg' : '$lbs lbs';
final secondaryText = _useMetric ? '$lbs lbs' : '${_weightKg.round()} kg';
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
primaryText,
style: Theme.of(context).textTheme.headlineMedium,
),
Text(
secondaryText,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
Slider(
value: _weightKg,
min: 30,
max: 200,
divisions: 374,
onChanged: widget.readOnly
? null
: (value) => setState(() => _weightKg = value),
),
],
);
}
Widget _buildBmiDisplay() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.surfaceVariant,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'BMI',
style: Theme.of(context).textTheme.bodyMedium,
),
Text(
_bmi.toStringAsFixed(1),
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _getBmiColor(),
borderRadius: BorderRadius.circular(8),
),
child: Text(
_bmiCategory,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
Color _getBmiColor() {
if (_bmi < 18.5 || _bmi >= 30) return AppColors.warning;
if (_bmi >= 25) return AppColors.primary;
return AppColors.success;
}
Widget _buildDiagnosisCheckboxes() {
return Column(
children: Diagnosis.values.map((diagnosis) {
return CheckboxListTile(
value: _diagnoses.contains(diagnosis),
onChanged: widget.readOnly
? null
: (checked) {
setState(() {
if (checked == true) {
_diagnoses.add(diagnosis);
} else {
_diagnoses.remove(diagnosis);
}
});
},
title: Text(_getDiagnosisLabel(diagnosis)),
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
);
}).toList(),
);
}
String _getDiagnosisLabel(Diagnosis diagnosis) {
switch (diagnosis) {
case Diagnosis.cardiovascular:
return 'Cardiovascular disease';
case Diagnosis.diabetes:
return 'Diabetes';
case Diagnosis.cancer:
return 'Cancer (active)';
case Diagnosis.copd:
return 'COPD';
case Diagnosis.hypertension:
return 'Hypertension';
}
}
String _cmToFeetInches(double cm) {
final totalInches = cm / 2.54;
final feet = (totalInches / 12).floor();
final inches = (totalInches % 12).round();
return "$feet'$inches\"";
}
Future<void> _toggleUnits() async {
final newValue = !_useMetric;
await LocalStorage.setUseMetricUnits(newValue);
setState(() => _useMetric = newValue);
}
void _continue() async {
final profile = UserProfile(
age: _age,
sex: _sex,
country: _country,
heightCm: _heightCm,
weightKg: _weightKg,
diagnoses: _diagnoses,
);
await LocalStorage.saveProfile(profile);
if (mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => BehavioralScreen(
profile: profile,
initialBehaviors: widget.initialBehaviors,
),
),
);
}
}
}