Implement initial features for husband's companion app, including mock data service and husband notes screen. Refactor scripture and cycle services for improved stability and testability. Address iOS Safari web app startup issue by removing deprecated initialization. - Implemented MockDataService and HusbandNotesScreen. - Converted _DashboardTab and DevotionalScreen to StatefulWidgets for robust scripture provider initialization. - Refactored CycleService to use immutable CycleInfo class, reducing UI rebuilds. - Removed deprecated window.flutterConfiguration from index.html, resolving Flutter web app startup failure on iOS Safari. - Updated and fixed related tests.
837 lines
30 KiB
Dart
837 lines
30 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import '../../models/cycle_entry.dart';
|
|
import '../../providers/navigation_provider.dart';
|
|
import '../../providers/user_provider.dart';
|
|
import '../../theme/app_theme.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
class LogScreen extends ConsumerStatefulWidget {
|
|
final DateTime? initialDate;
|
|
const LogScreen({super.key, this.initialDate});
|
|
|
|
@override
|
|
ConsumerState<LogScreen> createState() => _LogScreenState();
|
|
}
|
|
|
|
class _LogScreenState extends ConsumerState<LogScreen> {
|
|
late DateTime _selectedDate;
|
|
String? _existingEntryId;
|
|
bool _isPeriodDay = false;
|
|
FlowIntensity? _flowIntensity;
|
|
MoodLevel? _mood;
|
|
int? _energyLevel;
|
|
int _crampIntensity = 0;
|
|
bool _hasHeadache = false;
|
|
bool _hasBloating = false;
|
|
bool _hasBreastTenderness = false;
|
|
bool _hasFatigue = false;
|
|
bool _hasAcne = false;
|
|
bool _hasLowerBackPain = false;
|
|
bool _hasConstipation = false;
|
|
bool _hasDiarrhea = false;
|
|
bool _hasInsomnia = false;
|
|
int? _stressLevel;
|
|
final TextEditingController _notesController = TextEditingController();
|
|
final TextEditingController _cravingsController = TextEditingController();
|
|
|
|
// Intimacy tracking
|
|
bool _hadIntimacy = false;
|
|
bool? _intimacyProtected; // null = no selection, true = protected, false = unprotected
|
|
|
|
// Hidden field to preserve husband's notes
|
|
String? _husbandNotes;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_selectedDate = widget.initialDate ?? DateTime.now();
|
|
|
|
// Defer data loading to avoid build-time ref.read
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_loadExistingData();
|
|
});
|
|
}
|
|
|
|
void _loadExistingData() {
|
|
final entries = ref.read(cycleEntriesProvider);
|
|
try {
|
|
final entry = entries.firstWhere(
|
|
(e) => DateUtils.isSameDay(e.date, _selectedDate),
|
|
);
|
|
setState(() {
|
|
_existingEntryId = entry.id;
|
|
_isPeriodDay = entry.isPeriodDay;
|
|
_flowIntensity = entry.flowIntensity;
|
|
_mood = entry.mood;
|
|
_energyLevel = entry.energyLevel;
|
|
_crampIntensity = entry.crampIntensity ?? 0;
|
|
_hasHeadache = entry.hasHeadache;
|
|
_hasBloating = entry.hasBloating;
|
|
_hasBreastTenderness = entry.hasBreastTenderness;
|
|
_hasFatigue = entry.hasFatigue;
|
|
_hasAcne = entry.hasAcne;
|
|
_hasLowerBackPain = entry.hasLowerBackPain;
|
|
_hasConstipation = entry.hasConstipation;
|
|
_hasDiarrhea = entry.hasDiarrhea;
|
|
_hasInsomnia = entry.hasInsomnia;
|
|
_stressLevel = entry.stressLevel;
|
|
_notesController.text = entry.notes ?? '';
|
|
_cravingsController.text = entry.cravings?.join(', ') ?? '';
|
|
_husbandNotes = entry.husbandNotes;
|
|
_hadIntimacy = entry.hadIntimacy;
|
|
_intimacyProtected = entry.intimacyProtected;
|
|
});
|
|
} catch (_) {
|
|
// No existing entry for this day
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_notesController.dispose();
|
|
_cravingsController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _saveEntry() async {
|
|
List<String>? cravings;
|
|
if (_cravingsController.text.isNotEmpty) {
|
|
cravings = _cravingsController.text
|
|
.split(',')
|
|
.map((e) => e.trim())
|
|
.where((e) => e.isNotEmpty)
|
|
.toList();
|
|
}
|
|
|
|
final entry = CycleEntry(
|
|
id: _existingEntryId ?? const Uuid().v4(),
|
|
date: _selectedDate,
|
|
isPeriodDay: _isPeriodDay,
|
|
flowIntensity: _isPeriodDay ? _flowIntensity : null,
|
|
mood: _mood,
|
|
energyLevel: _energyLevel,
|
|
crampIntensity: _crampIntensity > 0 ? _crampIntensity : null,
|
|
hasHeadache: _hasHeadache,
|
|
hasBloating: _hasBloating,
|
|
hasBreastTenderness: _hasBreastTenderness,
|
|
hasFatigue: _hasFatigue,
|
|
hasAcne: _hasAcne,
|
|
hasLowerBackPain: _hasLowerBackPain,
|
|
hasConstipation: _hasConstipation,
|
|
hasDiarrhea: _hasDiarrhea,
|
|
hasInsomnia: _hasInsomnia,
|
|
stressLevel: _stressLevel,
|
|
notes: _notesController.text.isNotEmpty ? _notesController.text : null,
|
|
cravings: cravings,
|
|
husbandNotes: _husbandNotes,
|
|
hadIntimacy: _hadIntimacy,
|
|
intimacyProtected: _hadIntimacy ? _intimacyProtected : null,
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
);
|
|
|
|
if (_existingEntryId != null) {
|
|
await ref.read(cycleEntriesProvider.notifier).updateEntry(entry);
|
|
} else {
|
|
await ref.read(cycleEntriesProvider.notifier).addEntry(entry);
|
|
}
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Entry saved!', style: GoogleFonts.outfit()),
|
|
backgroundColor: AppColors.success,
|
|
behavior: SnackBarBehavior.floating,
|
|
shape:
|
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
);
|
|
if (widget.initialDate != null) {
|
|
Navigator.pop(context);
|
|
} else {
|
|
_resetForm();
|
|
}
|
|
}
|
|
}
|
|
|
|
void _resetForm() {
|
|
setState(() {
|
|
_existingEntryId = null;
|
|
_isPeriodDay = false;
|
|
_flowIntensity = null;
|
|
_mood = null;
|
|
_energyLevel = 3;
|
|
_crampIntensity = 0;
|
|
_hasHeadache = false;
|
|
_hasBloating = false;
|
|
_hasBreastTenderness = false;
|
|
_hasFatigue = false;
|
|
_hasAcne = false;
|
|
_hasLowerBackPain = false;
|
|
_hasConstipation = false;
|
|
_hasDiarrhea = false;
|
|
_hasInsomnia = false;
|
|
_stressLevel = 1;
|
|
_notesController.clear();
|
|
_cravingsController.clear();
|
|
_husbandNotes = null;
|
|
_hadIntimacy = false;
|
|
_intimacyProtected = null;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final isDark = theme.brightness == Brightness.dark;
|
|
|
|
return SafeArea(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Header
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'How are you feeling?',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 28,
|
|
fontWeight: FontWeight.w600,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
Text(
|
|
_formatDate(_selectedDate),
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
if (widget.initialDate == null)
|
|
IconButton(
|
|
onPressed: () =>
|
|
ref.read(navigationProvider.notifier).setIndex(0),
|
|
icon: const Icon(Icons.close),
|
|
style: IconButton.styleFrom(
|
|
backgroundColor:
|
|
theme.colorScheme.surfaceVariant.withOpacity(0.5),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Period Toggle
|
|
_buildSectionCard(
|
|
context,
|
|
title: 'Period',
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
'Is today a period day?',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 16,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
Switch(
|
|
value: _isPeriodDay,
|
|
onChanged: (value) => setState(() => _isPeriodDay = value),
|
|
activeColor: AppColors.menstrualPhase,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Flow Intensity (only if period day)
|
|
if (_isPeriodDay) ...[
|
|
const SizedBox(height: 16),
|
|
_buildSectionCard(
|
|
context,
|
|
title: 'Flow Intensity',
|
|
child: Row(
|
|
children: FlowIntensity.values.map((flow) {
|
|
final isSelected = _flowIntensity == flow;
|
|
return Expanded(
|
|
child: GestureDetector(
|
|
onTap: () => setState(() => _flowIntensity = flow),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? AppColors.menstrualPhase
|
|
.withOpacity(isDark ? 0.3 : 0.2)
|
|
: theme.colorScheme.surfaceVariant
|
|
.withOpacity(0.3),
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: isSelected
|
|
? Border.all(color: AppColors.menstrualPhase)
|
|
: Border.all(color: Colors.transparent),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Icon(
|
|
Icons.water_drop,
|
|
color: isSelected
|
|
? AppColors.menstrualPhase
|
|
: theme.colorScheme.onSurfaceVariant,
|
|
size: 20,
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
flow.label,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 11,
|
|
fontWeight: isSelected
|
|
? FontWeight.w600
|
|
: FontWeight.w400,
|
|
color: isSelected
|
|
? AppColors.menstrualPhase
|
|
: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
],
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Mood
|
|
_buildSectionCard(
|
|
context,
|
|
title: 'Mood',
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: MoodLevel.values.map((mood) {
|
|
final isSelected = _mood == mood;
|
|
return GestureDetector(
|
|
onTap: () => setState(() => _mood = mood),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? AppColors.softGold
|
|
.withOpacity(isDark ? 0.3 : 0.2)
|
|
: Colors.transparent,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: isSelected
|
|
? Border.all(color: AppColors.softGold)
|
|
: Border.all(color: Colors.transparent),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
mood.emoji,
|
|
style: TextStyle(
|
|
fontSize: isSelected ? 32 : 28,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
mood.label,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 10,
|
|
fontWeight: isSelected
|
|
? FontWeight.w600
|
|
: FontWeight.w400,
|
|
color: isSelected
|
|
? AppColors.softGold
|
|
: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Energy & Stress Levels
|
|
_buildSectionCard(
|
|
context,
|
|
title: 'Daily Levels',
|
|
child: Column(
|
|
children: [
|
|
// Energy Level
|
|
Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 80,
|
|
child: Text(
|
|
'Energy',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Slider(
|
|
value: (_energyLevel ?? 3).toDouble(),
|
|
min: 1,
|
|
max: 5,
|
|
divisions: 4,
|
|
activeColor: AppColors.sageGreen,
|
|
onChanged: (value) {
|
|
setState(() => _energyLevel = value.round());
|
|
},
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 50,
|
|
child: Text(
|
|
_getEnergyLabel(_energyLevel),
|
|
textAlign: TextAlign.end,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 11,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
// Stress Level
|
|
Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 80,
|
|
child: Text(
|
|
'Stress',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Slider(
|
|
value: (_stressLevel ?? 1).toDouble(),
|
|
min: 1,
|
|
max: 5,
|
|
divisions: 4,
|
|
activeColor: AppColors.ovulationPhase,
|
|
onChanged: (value) {
|
|
setState(() => _stressLevel = value.round());
|
|
},
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 50,
|
|
child: Text(
|
|
'${_stressLevel ?? 1}/5',
|
|
textAlign: TextAlign.end,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 12,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Symptoms
|
|
_buildSectionCard(
|
|
context,
|
|
title: 'Symptoms',
|
|
child: Column(
|
|
children: [
|
|
// Cramps Slider
|
|
Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 80,
|
|
child: Text(
|
|
'Cramps',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Slider(
|
|
value: _crampIntensity.toDouble(),
|
|
min: 0,
|
|
max: 5,
|
|
divisions: 5,
|
|
activeColor: AppColors.rose,
|
|
onChanged: (value) {
|
|
setState(() => _crampIntensity = value.round());
|
|
},
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 50,
|
|
child: Text(
|
|
_crampIntensity == 0
|
|
? 'None'
|
|
: '$_crampIntensity/5',
|
|
textAlign: TextAlign.end,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 11,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
// Symptom Toggles
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: [
|
|
_buildSymptomChip(context, 'Headache', _hasHeadache,
|
|
(v) => setState(() => _hasHeadache = v)),
|
|
_buildSymptomChip(context, 'Bloating', _hasBloating,
|
|
(v) => setState(() => _hasBloating = v)),
|
|
_buildSymptomChip(context, 'Breast Tenderness',
|
|
_hasBreastTenderness,
|
|
(v) => setState(() => _hasBreastTenderness = v)),
|
|
_buildSymptomChip(context, 'Fatigue', _hasFatigue,
|
|
(v) => setState(() => _hasFatigue = v)),
|
|
_buildSymptomChip(context, 'Acne', _hasAcne,
|
|
(v) => setState(() => _hasAcne = v)),
|
|
_buildSymptomChip(context, 'Back Pain',
|
|
_hasLowerBackPain,
|
|
(v) => setState(() => _hasLowerBackPain = v)),
|
|
_buildSymptomChip(
|
|
context,
|
|
'Constipation',
|
|
_hasConstipation,
|
|
(v) => setState(() => _hasConstipation = v)),
|
|
_buildSymptomChip(context, 'Diarrhea', _hasDiarrhea,
|
|
(v) => setState(() => _hasDiarrhea = v)),
|
|
_buildSymptomChip(context, 'Insomnia', _hasInsomnia,
|
|
(v) => setState(() => _hasInsomnia = v)),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Cravings
|
|
_buildSectionCard(
|
|
context,
|
|
title: 'Cravings',
|
|
child: TextField(
|
|
controller: _cravingsController,
|
|
decoration: InputDecoration(
|
|
hintText: 'e.g., Chocolate, salty chips (comma separated)',
|
|
filled: true,
|
|
fillColor: isDark
|
|
? theme.colorScheme.surface
|
|
: theme.colorScheme.surfaceVariant.withOpacity(0.1),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
),
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Intimacy Tracking (for married users)
|
|
_buildSectionCard(
|
|
context,
|
|
title: 'Intimacy',
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SwitchListTile(
|
|
title: Text('Had Intimacy Today', style: GoogleFonts.outfit(fontSize: 14)),
|
|
value: _hadIntimacy,
|
|
onChanged: (val) => setState(() {
|
|
_hadIntimacy = val;
|
|
if (!val) _intimacyProtected = null;
|
|
}),
|
|
activeColor: AppColors.sageGreen,
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
if (_hadIntimacy) ...[
|
|
const SizedBox(height: 8),
|
|
Text('Protection:', style: GoogleFonts.outfit(fontSize: 13, color: AppColors.warmGray)),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: GestureDetector(
|
|
onTap: () => setState(() => _intimacyProtected = true),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: _intimacyProtected == true
|
|
? AppColors.sageGreen.withOpacity(0.2)
|
|
: Colors.grey.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: _intimacyProtected == true
|
|
? AppColors.sageGreen
|
|
: Colors.grey.withOpacity(0.3),
|
|
),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
'Protected',
|
|
style: GoogleFonts.outfit(
|
|
fontWeight: FontWeight.w500,
|
|
color: _intimacyProtected == true
|
|
? AppColors.sageGreen
|
|
: AppColors.warmGray,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: GestureDetector(
|
|
onTap: () => setState(() => _intimacyProtected = false),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: _intimacyProtected == false
|
|
? AppColors.rose.withOpacity(0.15)
|
|
: Colors.grey.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: _intimacyProtected == false
|
|
? AppColors.rose
|
|
: Colors.grey.withOpacity(0.3),
|
|
),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
'Unprotected',
|
|
style: GoogleFonts.outfit(
|
|
fontWeight: FontWeight.w500,
|
|
color: _intimacyProtected == false
|
|
? AppColors.rose
|
|
: AppColors.warmGray,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Notes
|
|
_buildSectionCard(
|
|
context,
|
|
title: 'Notes',
|
|
child: TextField(
|
|
controller: _notesController,
|
|
maxLines: 3,
|
|
decoration: InputDecoration(
|
|
hintText: 'Add any notes about how you\'re feeling...',
|
|
filled: true,
|
|
fillColor: isDark
|
|
? theme.colorScheme.surface
|
|
: theme.colorScheme.surfaceVariant.withOpacity(0.1),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
),
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
// Save Button
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 54,
|
|
child: ElevatedButton(
|
|
onPressed: _saveEntry,
|
|
child: const Text('Save Entry'),
|
|
),
|
|
),
|
|
const SizedBox(height: 40),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSectionCard(BuildContext context,
|
|
{required String title, required Widget child}) {
|
|
final theme = Theme.of(context);
|
|
final isDark = theme.brightness == Brightness.dark;
|
|
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: theme.cardTheme.color,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: theme.colorScheme.outline.withOpacity(0.05)),
|
|
boxShadow: isDark
|
|
? null
|
|
: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
child,
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSymptomChip(BuildContext context, String label, bool isSelected,
|
|
ValueChanged<bool> onChanged) {
|
|
final theme = Theme.of(context);
|
|
final isDark = theme.brightness == Brightness.dark;
|
|
|
|
return Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: () => onChanged(!isSelected),
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? theme.colorScheme.tertiary.withOpacity(isDark ? 0.3 : 0.2)
|
|
: theme.colorScheme.surfaceVariant.withOpacity(0.3),
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: isSelected
|
|
? Border.all(color: theme.colorScheme.tertiary)
|
|
: Border.all(color: Colors.transparent),
|
|
),
|
|
child: Text(
|
|
label,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 13,
|
|
color: isSelected
|
|
? theme.colorScheme.onSurface
|
|
: theme.colorScheme.onSurfaceVariant,
|
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
String _formatDate(DateTime date) {
|
|
final now = DateTime.now();
|
|
if (DateUtils.isSameDay(date, now)) {
|
|
return 'Today, ${_getMonth(date.month)} ${date.day}';
|
|
}
|
|
const days = [
|
|
'Monday',
|
|
'Tuesday',
|
|
'Wednesday',
|
|
'Thursday',
|
|
'Friday',
|
|
'Saturday',
|
|
'Sunday'
|
|
];
|
|
return '${days[date.weekday - 1]}, ${_getMonth(date.month)} ${date.day}';
|
|
}
|
|
|
|
String _getMonth(int month) {
|
|
const months = [
|
|
'January',
|
|
'February',
|
|
'March',
|
|
'April',
|
|
'May',
|
|
'June',
|
|
'July',
|
|
'August',
|
|
'September',
|
|
'October',
|
|
'November',
|
|
'December'
|
|
];
|
|
return months[month - 1];
|
|
}
|
|
|
|
String _getEnergyLabel(int? level) {
|
|
if (level == null) return 'Not logged';
|
|
switch (level) {
|
|
case 1:
|
|
return 'Very Low';
|
|
case 2:
|
|
return 'Low';
|
|
case 3:
|
|
return 'Normal';
|
|
case 4:
|
|
return 'Good';
|
|
case 5:
|
|
return 'Excellent';
|
|
default:
|
|
return 'Normal';
|
|
}
|
|
}
|
|
}
|