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.
1583 lines
56 KiB
Dart
1583 lines
56 KiB
Dart
import 'package:christian_period_tracker/models/user_profile.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import '../../theme/app_theme.dart';
|
|
import '../../models/cycle_entry.dart';
|
|
import '../../models/scripture.dart';
|
|
import '../../providers/user_provider.dart';
|
|
import '../../services/cycle_service.dart';
|
|
import '../../services/mock_data_service.dart'; // Import mock service
|
|
import '../calendar/calendar_screen.dart'; // Import calendar
|
|
import 'husband_notes_screen.dart'; // Import notes screen
|
|
import 'learn_article_screen.dart'; // Import learn article screen
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
/// Husband's companion app main screen
|
|
class HusbandHomeScreen extends ConsumerStatefulWidget {
|
|
const HusbandHomeScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<HusbandHomeScreen> createState() => _HusbandHomeScreenState();
|
|
}
|
|
|
|
class _HusbandHomeScreenState extends ConsumerState<HusbandHomeScreen> {
|
|
int _selectedIndex = 0;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Theme(
|
|
data: _husbandTheme,
|
|
child: Scaffold(
|
|
backgroundColor: AppColors.warmCream,
|
|
body: IndexedStack(
|
|
index: _selectedIndex,
|
|
children: [
|
|
const _HusbandDashboard(),
|
|
const CalendarScreen(readOnly: true), // Reused Calendar
|
|
const HusbandNotesScreen(), // Notes Screen
|
|
const _HusbandTipsScreen(),
|
|
const _HusbandLearnScreen(),
|
|
const _HusbandSettingsScreen(),
|
|
],
|
|
),
|
|
bottomNavigationBar: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: AppColors.navyBlue.withOpacity(0.1),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, -2),
|
|
),
|
|
],
|
|
),
|
|
child: BottomNavigationBar(
|
|
currentIndex: _selectedIndex,
|
|
onTap: (index) => setState(() => _selectedIndex = index),
|
|
backgroundColor: Colors.white,
|
|
selectedItemColor: AppColors.navyBlue,
|
|
unselectedItemColor: AppColors.warmGray,
|
|
type: BottomNavigationBarType.fixed,
|
|
items: const [
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.home_outlined),
|
|
activeIcon: Icon(Icons.home),
|
|
label: 'Home',
|
|
),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.calendar_month_outlined),
|
|
activeIcon: Icon(Icons.calendar_month),
|
|
label: 'Calendar',
|
|
),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.note_alt_outlined),
|
|
activeIcon: Icon(Icons.note_alt),
|
|
label: 'Notes',
|
|
),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.lightbulb_outline),
|
|
activeIcon: Icon(Icons.lightbulb),
|
|
label: 'Tips',
|
|
),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.school_outlined),
|
|
activeIcon: Icon(Icons.school),
|
|
label: 'Learn',
|
|
),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.settings_outlined),
|
|
activeIcon: Icon(Icons.settings),
|
|
label: 'Settings',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
ThemeData get _husbandTheme {
|
|
return ThemeData(
|
|
useMaterial3: true,
|
|
brightness: Brightness.light,
|
|
scaffoldBackgroundColor: AppColors.warmCream,
|
|
colorScheme: const ColorScheme.light(
|
|
primary: AppColors.navyBlue,
|
|
secondary: AppColors.gold,
|
|
surface: AppColors.warmCream,
|
|
),
|
|
appBarTheme: AppBarTheme(
|
|
backgroundColor: AppColors.warmCream,
|
|
foregroundColor: AppColors.navyBlue,
|
|
elevation: 0,
|
|
titleTextStyle: GoogleFonts.outfit(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.navyBlue,
|
|
),
|
|
),
|
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.navyBlue,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _HusbandDashboard extends ConsumerStatefulWidget {
|
|
const _HusbandDashboard();
|
|
|
|
@override
|
|
ConsumerState<_HusbandDashboard> createState() => _HusbandDashboardState();
|
|
}
|
|
|
|
class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> {
|
|
Scripture? _currentScripture;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadNewVerse();
|
|
}
|
|
|
|
void _loadNewVerse() {
|
|
setState(() {
|
|
_currentScripture = ScriptureDatabase().getHusbandScripture();
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final user = ref.watch(userProfileProvider);
|
|
final cycleInfo = ref.watch(currentCycleInfoProvider);
|
|
|
|
final wifeName = user?.partnerName ?? "Wife";
|
|
final phase = cycleInfo.phase;
|
|
final dayOfCycle = cycleInfo.dayOfCycle;
|
|
final daysUntilPeriod = cycleInfo.daysUntilPeriod;
|
|
|
|
final scripture = _currentScripture ?? ScriptureDatabase().getHusbandScripture();
|
|
|
|
return SafeArea(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Greeting
|
|
Text(
|
|
'Hey there,',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 16,
|
|
color: AppColors.warmGray,
|
|
),
|
|
),
|
|
Text(
|
|
'Husband',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 28,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.navyBlue,
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Wife's Cycle Status
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
AppColors.navyBlue,
|
|
AppColors.steelBlue,
|
|
],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: const Icon(
|
|
Icons.favorite,
|
|
color: Colors.white,
|
|
size: 22,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Text(
|
|
'$wifeName\'s Cycle',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.white.withOpacity(0.9),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Day $dayOfCycle • ${phase.label}',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
daysUntilPeriod > 0
|
|
? '~$daysUntilPeriod days until period'
|
|
: 'Period expected soon',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: Colors.white.withOpacity(0.8),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 6,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(phase.emoji),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
_getPhaseHint(phase),
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 12,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Support Tip
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: AppColors.navyBlue.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 36,
|
|
height: 36,
|
|
decoration: BoxDecoration(
|
|
color: AppColors.gold.withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: const Icon(
|
|
Icons.lightbulb_outline,
|
|
color: AppColors.gold,
|
|
size: 20,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Text(
|
|
'How to Support Her',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.navyBlue,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
_getSupportTip(phase),
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: AppColors.charcoal,
|
|
height: 1.5,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Recent Cravings (Dynamic)
|
|
Builder(
|
|
builder: (context) {
|
|
// Get recent cravings from the last 3 days
|
|
final allEntries = ref.read(cycleEntriesProvider);
|
|
// Sort by date desc
|
|
final sortedEntries = List<CycleEntry>.from(allEntries)..sort((a,b) => b.date.compareTo(a.date));
|
|
|
|
final recentCravings = <String>{};
|
|
final now = DateTime.now();
|
|
for (var entry in sortedEntries) {
|
|
if (now.difference(entry.date).inDays > 3) break;
|
|
if (entry.cravings != null) {
|
|
recentCravings.addAll(entry.cravings!);
|
|
}
|
|
}
|
|
|
|
if (recentCravings.isEmpty) return const SizedBox.shrink();
|
|
|
|
return Container(
|
|
width: double.infinity,
|
|
margin: const EdgeInsets.only(bottom: 20),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: AppColors.rose.withOpacity(0.3)),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: AppColors.rose.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 36,
|
|
height: 36,
|
|
decoration: BoxDecoration(
|
|
color: AppColors.rose.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Icon(
|
|
Icons.fastfood,
|
|
color: AppColors.rose,
|
|
size: 20,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Text(
|
|
'She is Craving...',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.navyBlue,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: recentCravings.map((craving) => Chip(
|
|
label: Text(craving),
|
|
backgroundColor: AppColors.rose.withOpacity(0.1),
|
|
labelStyle: GoogleFonts.outfit(color: AppColors.navyBlue, fontWeight: FontWeight.w500),
|
|
side: BorderSide.none,
|
|
)).toList(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
|
|
// Scripture for Husbands
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
AppColors.gold.withOpacity(0.15),
|
|
AppColors.warmCream,
|
|
],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(
|
|
color: AppColors.gold.withOpacity(0.3),
|
|
),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.menu_book,
|
|
color: AppColors.gold,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'Scripture for Husbands',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: AppColors.warmGray,
|
|
),
|
|
),
|
|
),
|
|
// Quick version toggle
|
|
GestureDetector(
|
|
onTap: () => _showVersionPicker(context, ref),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.gold.withOpacity(0.15),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
user?.bibleTranslation.label ?? 'ESV',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.gold,
|
|
),
|
|
),
|
|
const SizedBox(width: 2),
|
|
Icon(Icons.arrow_drop_down, color: AppColors.gold, size: 16),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
'"${scripture.getVerse(user?.bibleTranslation ?? BibleTranslation.esv)}"',
|
|
style: GoogleFonts.lora(
|
|
fontSize: 15,
|
|
fontStyle: FontStyle.italic,
|
|
color: AppColors.navyBlue,
|
|
height: 1.6,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'— ${scripture.reference}',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
color: AppColors.warmGray,
|
|
),
|
|
),
|
|
GestureDetector(
|
|
onTap: _loadNewVerse,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.gold.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.refresh, color: AppColors.gold, size: 14),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
'New Verse',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w500,
|
|
color: AppColors.gold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Prayer Button
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: OutlinedButton.icon(
|
|
onPressed: () => _showPrayerPrompt(context, phase),
|
|
icon: const Text('🙏', style: TextStyle(fontSize: 18)),
|
|
label: Text(
|
|
'Pray for ${wifeName}',
|
|
style: GoogleFonts.outfit(fontWeight: FontWeight.w500),
|
|
),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: AppColors.navyBlue,
|
|
side: const BorderSide(color: AppColors.navyBlue),
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 40),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
String _getPhaseHint(CyclePhase phase) {
|
|
switch (phase) {
|
|
case CyclePhase.menstrual:
|
|
return 'She may need extra rest';
|
|
case CyclePhase.follicular:
|
|
return 'Energy is returning';
|
|
case CyclePhase.ovulation:
|
|
return 'Fertile window';
|
|
case CyclePhase.luteal:
|
|
return 'PMS may occur';
|
|
}
|
|
}
|
|
|
|
String _getSupportTip(CyclePhase phase) {
|
|
switch (phase) {
|
|
case CyclePhase.menstrual:
|
|
return 'This is a time when she needs extra care. Help with household tasks without being asked. '
|
|
'Bring her favorite warm drink, suggest low-key activities, and be extra patient.';
|
|
case CyclePhase.follicular:
|
|
return 'Her energy is returning! This is a great time to plan dates, work on projects together, '
|
|
'and affirm her strengths. She may be more talkative and social.';
|
|
case CyclePhase.ovulation:
|
|
return 'Prioritize connection time. Romance and quality time matter. '
|
|
'If you\'re trying to conceive, this is your fertile window.';
|
|
case CyclePhase.luteal:
|
|
return 'Be patient—PMS may affect her mood. Listen more, "fix" less. '
|
|
'Take initiative on responsibilities and surprise her with comfort foods.';
|
|
}
|
|
}
|
|
|
|
void _showVersionPicker(BuildContext context, WidgetRef ref) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
),
|
|
builder: (context) => Container(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Choose Translation',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.navyBlue,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
...BibleTranslation.values.map((translation) => ListTile(
|
|
title: Text(
|
|
translation.label,
|
|
style: GoogleFonts.outfit(fontWeight: FontWeight.w500),
|
|
),
|
|
trailing: ref.watch(userProfileProvider)?.bibleTranslation == translation
|
|
? const Icon(Icons.check, color: AppColors.sageGreen)
|
|
: null,
|
|
onTap: () async {
|
|
final profile = ref.read(userProfileProvider);
|
|
if (profile != null) {
|
|
await ref.read(userProfileProvider.notifier).updateProfile(
|
|
profile.copyWith(bibleTranslation: translation),
|
|
);
|
|
}
|
|
if (context.mounted) Navigator.pop(context);
|
|
},
|
|
)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showPrayerPrompt(BuildContext context, CyclePhase phase) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
),
|
|
builder: (context) => Container(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
'🙏 Prayer for Your Wife',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.navyBlue,
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
Text(
|
|
_getPrayer(phase),
|
|
textAlign: TextAlign.center,
|
|
style: GoogleFonts.lora(
|
|
fontSize: 16,
|
|
fontStyle: FontStyle.italic,
|
|
color: AppColors.charcoal,
|
|
height: 1.6,
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.navyBlue,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: const Text('Amen'),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
String _getPrayer(CyclePhase phase) {
|
|
switch (phase) {
|
|
case CyclePhase.menstrual:
|
|
return '"Lord, I lift up my wife during this time of rest. '
|
|
'Give her body the renewal it needs and grant her Your peace. '
|
|
'Help me to serve her with patience and love. Amen."';
|
|
case CyclePhase.follicular:
|
|
return '"Father, thank You for my wife\'s renewed energy. '
|
|
'Bless her endeavors and help me to encourage and support her. '
|
|
'May our partnership glorify You. Amen."';
|
|
case CyclePhase.ovulation:
|
|
return '"Creator God, You have designed my wife fearfully and wonderfully. '
|
|
'Whatever Your plans for our family, help us trust Your timing. '
|
|
'Bless our marriage and intimacy. Amen."';
|
|
case CyclePhase.luteal:
|
|
return '"Lord, be near to my wife during this phase. '
|
|
'When emotions are difficult, grant her Your peace that passes understanding. '
|
|
'Help me to be patient, kind, and understanding. Amen."';
|
|
}
|
|
}
|
|
}
|
|
|
|
class _HusbandTipsScreen extends StatelessWidget {
|
|
const _HusbandTipsScreen();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SafeArea(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Supporting Her',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 28,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.navyBlue,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Practical ways to love your wife well',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: AppColors.warmGray,
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
_buildTipCategory('During Her Period', [
|
|
'🏠 Help with household tasks without being asked',
|
|
'🍵 Bring her favorite comfort drink',
|
|
'📺 Suggest low-key activities (movies, quiet time)',
|
|
'🙏 Pray for her physical comfort',
|
|
]),
|
|
const SizedBox(height: 16),
|
|
_buildTipCategory('Follicular Phase', [
|
|
'🎉 Plan dates or activities—her energy is returning',
|
|
'💬 She may be more talkative and social',
|
|
'💪 Great time for projects together',
|
|
'❤️ Affirm her strengths and beauty',
|
|
]),
|
|
const SizedBox(height: 16),
|
|
_buildTipCategory('Luteal Phase (PMS)', [
|
|
'😌 Be patient—PMS may affect her mood',
|
|
'🍫 Surprise with comfort foods',
|
|
'🧹 Take initiative on responsibilities',
|
|
'👂 Listen more, "fix" less',
|
|
]),
|
|
const SizedBox(height: 16),
|
|
const SizedBox(height: 16),
|
|
|
|
// Her Favorites Section
|
|
Consumer(
|
|
builder: (context, ref, child) {
|
|
final user = ref.watch(userProfileProvider);
|
|
final favorites = user?.favoriteFoods;
|
|
|
|
if (favorites == null || favorites.isEmpty) return const SizedBox.shrink();
|
|
|
|
return Column(
|
|
children: [
|
|
_buildTipCategory('❤️ Her Favorites (Cheat Sheet)', favorites),
|
|
const SizedBox(height: 16),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
|
|
_buildTipCategory('General Wisdom', [
|
|
'🗣️ Ask how she\'s feeling—and actually listen',
|
|
'📱 Put your phone down when she\'s talking',
|
|
'🌹 Small gestures matter more than grand ones',
|
|
'🙏 Pray for her daily',
|
|
]),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTipCategory(String title, List<String> tips) {
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.navyBlue,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
...tips.map((tip) => Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: Text(
|
|
tip,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: AppColors.charcoal,
|
|
height: 1.4,
|
|
),
|
|
),
|
|
)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _HusbandLearnScreen extends StatelessWidget {
|
|
const _HusbandLearnScreen();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SafeArea(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Learn',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 28,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.navyBlue,
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
_buildSection(context, 'Understanding Her', [
|
|
_LearnItem(
|
|
icon: Icons.loop,
|
|
title: 'The 4 Phases of Her Cycle',
|
|
subtitle: 'What\'s happening in her body each month',
|
|
articleId: 'four_phases',
|
|
),
|
|
_LearnItem(
|
|
icon: Icons.psychology_outlined,
|
|
title: 'Why Does Her Mood Change?',
|
|
subtitle: 'Hormones explained simply',
|
|
articleId: 'mood_changes',
|
|
),
|
|
_LearnItem(
|
|
icon: Icons.medical_information_outlined,
|
|
title: 'PMS is Real',
|
|
subtitle: 'Medical facts for supportive husbands',
|
|
articleId: 'pms_is_real',
|
|
),
|
|
]),
|
|
const SizedBox(height: 24),
|
|
_buildSection(context, 'Biblical Manhood', [
|
|
_LearnItem(
|
|
icon: Icons.favorite,
|
|
title: 'Loving Like Christ',
|
|
subtitle: 'Ephesians 5 in daily practice',
|
|
articleId: 'loving_like_christ',
|
|
),
|
|
_LearnItem(
|
|
icon: Icons.handshake,
|
|
title: 'Servant Leadership at Home',
|
|
subtitle: 'What it really means',
|
|
articleId: 'servant_leadership',
|
|
),
|
|
_LearnItem(
|
|
icon: Icons.auto_awesome,
|
|
title: 'Praying for Your Wife',
|
|
subtitle: 'Practical guide',
|
|
articleId: 'praying_for_wife',
|
|
),
|
|
]),
|
|
const SizedBox(height: 24),
|
|
_buildSection(context, 'NFP for Husbands', [
|
|
_LearnItem(
|
|
icon: Icons.show_chart,
|
|
title: 'Reading the Charts Together',
|
|
subtitle: 'Understanding fertility signs',
|
|
articleId: 'reading_charts',
|
|
),
|
|
_LearnItem(
|
|
icon: Icons.schedule,
|
|
title: 'Abstinence as Spiritual Discipline',
|
|
subtitle: 'Growing together during fertile days',
|
|
articleId: 'abstinence_discipline',
|
|
),
|
|
]),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSection(BuildContext context, String title, List<_LearnItem> items) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: AppColors.warmGray,
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
children: items
|
|
.map((item) => ListTile(
|
|
leading: Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: AppColors.navyBlue.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Icon(
|
|
item.icon,
|
|
color: AppColors.navyBlue,
|
|
size: 20,
|
|
),
|
|
),
|
|
title: Text(
|
|
item.title,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w500,
|
|
color: AppColors.charcoal,
|
|
),
|
|
),
|
|
subtitle: Text(
|
|
item.subtitle,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 13,
|
|
color: AppColors.warmGray,
|
|
),
|
|
),
|
|
trailing: const Icon(
|
|
Icons.chevron_right,
|
|
color: AppColors.lightGray,
|
|
),
|
|
onTap: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => LearnArticleScreen(articleId: item.articleId),
|
|
),
|
|
);
|
|
},
|
|
))
|
|
.toList(),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _LearnItem {
|
|
final IconData icon;
|
|
final String title;
|
|
final String subtitle;
|
|
final String articleId;
|
|
|
|
const _LearnItem({
|
|
required this.icon,
|
|
required this.title,
|
|
required this.subtitle,
|
|
required this.articleId,
|
|
});
|
|
}
|
|
|
|
class _HusbandSettingsScreen extends ConsumerWidget {
|
|
const _HusbandSettingsScreen();
|
|
|
|
Future<void> _resetApp(BuildContext context, WidgetRef ref) async {
|
|
final confirmed = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Reset App?'),
|
|
content: const Text(
|
|
'This will clear all data and return you to onboarding. Are you sure?'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
child: const Text('Cancel')),
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
child: const Text('Reset', style: TextStyle(color: Colors.red)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirmed == true) {
|
|
await ref.read(userProfileProvider.notifier).clearProfile();
|
|
await ref.read(cycleEntriesProvider.notifier).clearEntries();
|
|
|
|
if (context.mounted) {
|
|
Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _loadDemoData(BuildContext context, WidgetRef ref) async {
|
|
final confirmed = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Load Demo Data?'),
|
|
content: const Text(
|
|
'This will populate the app with mock cycle entries and a wife profile.'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
child: const Text('Cancel')),
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
child:
|
|
const Text('Load Data', style: TextStyle(color: Colors.blue)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirmed == true) {
|
|
final mockService = MockDataService();
|
|
// Load mock entries
|
|
final entries = mockService.generateMockCycleEntries();
|
|
for (var entry in entries) {
|
|
await ref.read(cycleEntriesProvider.notifier).addEntry(entry);
|
|
}
|
|
|
|
// Update mock profile
|
|
final mockWife = mockService.generateMockWifeProfile();
|
|
// Need to preserve current Husband ID and Role but take other data
|
|
final currentProfile = ref.read(userProfileProvider);
|
|
if (currentProfile != null) {
|
|
final updatedProfile = currentProfile.copyWith(
|
|
partnerName: mockWife.name,
|
|
averageCycleLength: mockWife.averageCycleLength,
|
|
averagePeriodLength: mockWife.averagePeriodLength,
|
|
lastPeriodStartDate: mockWife.lastPeriodStartDate,
|
|
favoriteFoods: mockWife.favoriteFoods,
|
|
);
|
|
await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _showTranslationPicker(BuildContext context, WidgetRef ref) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
),
|
|
builder: (context) => Container(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Choose Translation',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.navyBlue,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
...BibleTranslation.values.map((translation) => ListTile(
|
|
title: Text(
|
|
translation.label,
|
|
style: GoogleFonts.outfit(fontWeight: FontWeight.w500),
|
|
),
|
|
trailing: ref.watch(userProfileProvider)?.bibleTranslation == translation
|
|
? const Icon(Icons.check, color: AppColors.sageGreen)
|
|
: null,
|
|
onTap: () async {
|
|
final profile = ref.read(userProfileProvider);
|
|
if (profile != null) {
|
|
await ref.read(userProfileProvider.notifier).updateProfile(
|
|
profile.copyWith(bibleTranslation: translation),
|
|
);
|
|
}
|
|
if (context.mounted) Navigator.pop(context);
|
|
},
|
|
)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showConnectDialog(BuildContext context, WidgetRef ref) {
|
|
final codeController = TextEditingController();
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Row(
|
|
children: [
|
|
const Icon(Icons.link, color: AppColors.navyBlue),
|
|
const SizedBox(width: 8),
|
|
const Text('Connect with Wife'),
|
|
],
|
|
),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Enter the pairing code from your wife\'s app:',
|
|
style: GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray),
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextField(
|
|
controller: codeController,
|
|
decoration: const InputDecoration(
|
|
hintText: 'e.g., ABC123',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
textCapitalization: TextCapitalization.characters,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Your wife can find this code in her Settings under "Share with Husband".',
|
|
style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('Cancel'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
final code = codeController.text.trim();
|
|
if (code.isEmpty) return;
|
|
|
|
// In a real app, this would validate the code against a backend
|
|
// For now, we'll just show a success message and simulate pairing
|
|
Navigator.pop(context);
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Connected! Loading wife\'s data...'),
|
|
backgroundColor: AppColors.sageGreen,
|
|
),
|
|
);
|
|
|
|
// Load demo data as simulation of pairing
|
|
final mockService = MockDataService();
|
|
final entries = mockService.generateMockCycleEntries();
|
|
for (var entry in entries) {
|
|
await ref.read(cycleEntriesProvider.notifier).addEntry(entry);
|
|
}
|
|
final mockWife = mockService.generateMockWifeProfile();
|
|
final currentProfile = ref.read(userProfileProvider);
|
|
if (currentProfile != null) {
|
|
final updatedProfile = currentProfile.copyWith(
|
|
partnerName: mockWife.name,
|
|
averageCycleLength: mockWife.averageCycleLength,
|
|
averagePeriodLength: mockWife.averagePeriodLength,
|
|
lastPeriodStartDate: mockWife.lastPeriodStartDate,
|
|
favoriteFoods: mockWife.favoriteFoods,
|
|
);
|
|
await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
|
|
}
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.navyBlue,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: const Text('Connect'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return SafeArea(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Settings',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 28,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.navyBlue,
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
ListTile(
|
|
leading: const Icon(Icons.notifications_outlined,
|
|
color: AppColors.navyBlue),
|
|
title: Text('Notifications',
|
|
style: GoogleFonts.outfit(fontWeight: FontWeight.w500)),
|
|
trailing: Switch(value: true, onChanged: (val) {}),
|
|
),
|
|
const Divider(height: 1),
|
|
ListTile(
|
|
leading: const Icon(Icons.link,
|
|
color: AppColors.navyBlue),
|
|
title: Text('Connect with Wife',
|
|
style: GoogleFonts.outfit(fontWeight: FontWeight.w500)),
|
|
trailing: const Icon(Icons.chevron_right),
|
|
onTap: () => _showConnectDialog(context, ref),
|
|
),
|
|
const Divider(height: 1),
|
|
ListTile(
|
|
leading: const Icon(Icons.menu_book_outlined,
|
|
color: AppColors.navyBlue),
|
|
title: Text('Bible Translation',
|
|
style: GoogleFonts.outfit(fontWeight: FontWeight.w500)),
|
|
trailing: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
ref.watch(userProfileProvider.select((u) => u?.bibleTranslation.label)) ?? 'ESV',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: AppColors.warmGray,
|
|
),
|
|
),
|
|
const Icon(Icons.chevron_right),
|
|
],
|
|
),
|
|
onTap: () => _showTranslationPicker(context, ref),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
ListTile(
|
|
leading: const Icon(Icons.cloud_download_outlined,
|
|
color: Colors.blue),
|
|
title: Text('Load Demo Data',
|
|
style: GoogleFonts.outfit(
|
|
fontWeight: FontWeight.w500, color: Colors.blue)),
|
|
onTap: () => _loadDemoData(context, ref),
|
|
),
|
|
const Divider(height: 1),
|
|
ListTile(
|
|
leading: const Icon(Icons.logout, color: Colors.red),
|
|
title: Text('Reset App',
|
|
style: GoogleFonts.outfit(
|
|
fontWeight: FontWeight.w500, color: Colors.red)),
|
|
onTap: () => _resetApp(context, ref),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _HusbandWifeStatus extends ConsumerWidget {
|
|
const _HusbandWifeStatus();
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final user = ref.watch(userProfileProvider);
|
|
final cycleInfo = ref.watch(currentCycleInfoProvider);
|
|
final entries = ref.watch(cycleEntriesProvider);
|
|
|
|
final wifeName = user?.partnerName ?? "Wife";
|
|
final phase = cycleInfo.phase;
|
|
final dayOfCycle = cycleInfo.dayOfCycle;
|
|
|
|
// Find today's entry
|
|
final todayEntry = entries.firstWhere(
|
|
(e) => DateUtils.isSameDay(e.date, DateTime.now()),
|
|
orElse: () => CycleEntry(
|
|
id: '',
|
|
date: DateTime.now(),
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
),
|
|
);
|
|
|
|
return SafeArea(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Wife\'s Status',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 28,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.navyBlue,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Real-time updates on how $wifeName is doing',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: AppColors.warmGray,
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Phase and Day summary
|
|
Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: AppColors.navyBlue.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
_buildStatusCircle(dayOfCycle, phase),
|
|
const SizedBox(width: 20),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
phase.label,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.navyBlue,
|
|
),
|
|
),
|
|
Text(
|
|
'Cycle Day $dayOfCycle',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: AppColors.warmGray,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
phase.description,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 13,
|
|
color: AppColors.charcoal.withOpacity(0.8),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Symptoms for Today
|
|
if (todayEntry.hasSymptoms || todayEntry.mood != null) ...[
|
|
Text(
|
|
'Today\'s Logs',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.navyBlue,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.navyBlue.withOpacity(0.03),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border:
|
|
Border.all(color: AppColors.navyBlue.withOpacity(0.05)),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
if (todayEntry.mood != null)
|
|
_buildLogTile(Icons.emoji_emotions_outlined, 'Mood',
|
|
'${todayEntry.mood!.emoji} ${todayEntry.mood!.label}'),
|
|
if (todayEntry.hasSymptoms)
|
|
_buildLogTile(Icons.healing_outlined, 'Symptoms',
|
|
_getSymptomsSummary(todayEntry)),
|
|
if (todayEntry.energyLevel != null)
|
|
_buildLogTile(Icons.flash_on, 'Energy',
|
|
'${todayEntry.energyLevel}/5'),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
],
|
|
|
|
// Support Checklist
|
|
Text(
|
|
'Support Checklist',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.navyBlue,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
..._generateChecklist(todayEntry, phase)
|
|
.map((item) => _buildCheckItem(item)),
|
|
|
|
const SizedBox(height: 40),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatusCircle(int day, CyclePhase phase) {
|
|
return Container(
|
|
width: 70,
|
|
height: 70,
|
|
decoration: BoxDecoration(
|
|
color: phase.color.withOpacity(0.15),
|
|
shape: BoxShape.circle,
|
|
border: Border.all(color: phase.color.withOpacity(0.3), width: 2),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
day.toString(),
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.w700,
|
|
color: phase.color,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLogTile(IconData icon, String label, String value) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Row(
|
|
children: [
|
|
Icon(icon, size: 20, color: AppColors.steelBlue),
|
|
const SizedBox(width: 12),
|
|
Text(
|
|
'$label: ',
|
|
style: GoogleFonts.outfit(fontWeight: FontWeight.w500, fontSize: 14),
|
|
),
|
|
Expanded(
|
|
child: Text(
|
|
value,
|
|
style: GoogleFonts.outfit(fontSize: 14, color: AppColors.charcoal),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCheckItem(String text) {
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: AppColors.navyBlue.withOpacity(0.05)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.check_circle_outline,
|
|
color: AppColors.sageGreen, size: 20),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
text,
|
|
style: GoogleFonts.outfit(fontSize: 14, color: AppColors.navyBlue),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
String _getSymptomsSummary(CycleEntry entry) {
|
|
List<String> s = [];
|
|
if (entry.crampIntensity != null && entry.crampIntensity! > 0)
|
|
s.add('Cramps');
|
|
if (entry.hasHeadache) s.add('Headache');
|
|
if (entry.hasBloating) s.add('Bloating');
|
|
if (entry.hasFatigue) s.add('Fatigue');
|
|
if (entry.hasLowerBackPain) s.add('Back Pain');
|
|
return s.isNotEmpty ? s.join(', ') : 'None';
|
|
}
|
|
|
|
List<String> _generateChecklist(CycleEntry entry, CyclePhase phase) {
|
|
List<String> list = [];
|
|
|
|
// Symptom-based tips
|
|
if (entry.crampIntensity != null && entry.crampIntensity! >= 3) {
|
|
list.add('Bring her a heating pad or hot water bottle.');
|
|
}
|
|
if (entry.hasHeadache) {
|
|
list.add('Suggest some quiet time with dimmed lights.');
|
|
}
|
|
if (entry.hasFatigue ||
|
|
(entry.energyLevel != null && entry.energyLevel! <= 2)) {
|
|
list.add('Take over dinner or household chores tonight.');
|
|
}
|
|
if (entry.mood == MoodLevel.sad || entry.mood == MoodLevel.verySad) {
|
|
list.add('Offer a listening ear and extra comfort.');
|
|
}
|
|
|
|
// Phase-based fallback tips
|
|
if (list.length < 3) {
|
|
switch (phase) {
|
|
case CyclePhase.menstrual:
|
|
list.add('Suggest a relaxing movie night.');
|
|
list.add('Bring her a warm tea or cocoa.');
|
|
break;
|
|
case CyclePhase.follicular:
|
|
list.add('Plan a fun outdoor activity.');
|
|
list.add('Compliment her renewed energy.');
|
|
break;
|
|
case CyclePhase.ovulation:
|
|
list.add('Plan a romantic date night.');
|
|
list.add('Focus on quality connection time.');
|
|
break;
|
|
case CyclePhase.luteal:
|
|
list.add('Surprise her with her favorite comfort snack.');
|
|
list.add('Be extra patient if she\'s easily frustrated.');
|
|
break;
|
|
}
|
|
}
|
|
|
|
return list.take(4).toList();
|
|
}
|
|
}
|
|
|