Enhance Pad Tracking with new Flow and Supply logic
This commit is contained in:
@@ -62,17 +62,32 @@ class ChristianPeriodTrackerApp extends ConsumerWidget {
|
||||
final Color accentColor;
|
||||
|
||||
if (userProfile != null) {
|
||||
accentColor = _colorFromHex(userProfile.accentColor);
|
||||
switch (userProfile.themeMode) {
|
||||
case AppThemeMode.light:
|
||||
themeMode = ThemeMode.light;
|
||||
break;
|
||||
case AppThemeMode.dark:
|
||||
themeMode = ThemeMode.dark;
|
||||
|
||||
case AppThemeMode.system:
|
||||
themeMode = ThemeMode.system;
|
||||
break;
|
||||
if (userProfile.isHusband) {
|
||||
accentColor = _colorFromHex(userProfile.husbandAccentColor);
|
||||
switch (userProfile.husbandThemeMode) {
|
||||
case AppThemeMode.light:
|
||||
themeMode = ThemeMode.light;
|
||||
break;
|
||||
case AppThemeMode.dark:
|
||||
themeMode = ThemeMode.dark;
|
||||
break;
|
||||
case AppThemeMode.system:
|
||||
themeMode = ThemeMode.system;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
accentColor = _colorFromHex(userProfile.accentColor);
|
||||
switch (userProfile.themeMode) {
|
||||
case AppThemeMode.light:
|
||||
themeMode = ThemeMode.light;
|
||||
break;
|
||||
case AppThemeMode.dark:
|
||||
themeMode = ThemeMode.dark;
|
||||
break;
|
||||
case AppThemeMode.system:
|
||||
themeMode = ThemeMode.system;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Default theme for initial load or if profile is null
|
||||
|
||||
@@ -2,10 +2,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import '../models/user_profile.dart';
|
||||
import '../models/cycle_entry.dart';
|
||||
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../services/cycle_service.dart';
|
||||
|
||||
/// Provider for the user profile
|
||||
final userProfileProvider = StateNotifierProvider<UserProfileNotifier, UserProfile?>((ref) {
|
||||
final userProfileProvider =
|
||||
StateNotifierProvider<UserProfileNotifier, UserProfile?>((ref) {
|
||||
return UserProfileNotifier();
|
||||
});
|
||||
|
||||
@@ -38,9 +41,11 @@ class UserProfileNotifier extends StateNotifier<UserProfile?> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateRelationshipStatus(RelationshipStatus relationshipStatus) async {
|
||||
Future<void> updateRelationshipStatus(
|
||||
RelationshipStatus relationshipStatus) async {
|
||||
if (state != null) {
|
||||
await updateProfile(state!.copyWith(relationshipStatus: relationshipStatus));
|
||||
await updateProfile(
|
||||
state!.copyWith(relationshipStatus: relationshipStatus));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +57,8 @@ class UserProfileNotifier extends StateNotifier<UserProfile?> {
|
||||
}
|
||||
|
||||
/// Provider for cycle entries
|
||||
final cycleEntriesProvider = StateNotifierProvider<CycleEntriesNotifier, List<CycleEntry>>((ref) {
|
||||
final cycleEntriesProvider =
|
||||
StateNotifierProvider<CycleEntriesNotifier, List<CycleEntry>>((ref) {
|
||||
return CycleEntriesNotifier();
|
||||
});
|
||||
|
||||
@@ -102,6 +108,56 @@ class CycleEntriesNotifier extends StateNotifier<List<CycleEntry>> {
|
||||
await box.clear();
|
||||
state = [];
|
||||
}
|
||||
|
||||
Future<void> generateExampleData(String userId) async {
|
||||
await clearEntries();
|
||||
final box = Hive.box<CycleEntry>('cycle_entries');
|
||||
final today = DateTime.now();
|
||||
|
||||
// Generate 3 past cycles (~28 days each)
|
||||
DateTime cycleStart = today.subtract(const Duration(days: 84)); // 3 * 28
|
||||
|
||||
while (cycleStart.isBefore(today)) {
|
||||
// Create Period (5 days)
|
||||
for (int i = 0; i < 5; i++) {
|
||||
final date = cycleStart.add(Duration(days: i));
|
||||
if (date.isAfter(today)) break;
|
||||
|
||||
final isHeavy = i == 1 || i == 2;
|
||||
final entry = CycleEntry(
|
||||
id: const Uuid().v4(),
|
||||
date: date,
|
||||
isPeriodDay: true,
|
||||
flowIntensity: isHeavy ? FlowIntensity.heavy : FlowIntensity.medium,
|
||||
mood: i == 1 ? MoodLevel.sad : null,
|
||||
crampIntensity: i == 0 ? 3 : null,
|
||||
hasHeadache: i == 0,
|
||||
createdAt: date,
|
||||
updatedAt: date,
|
||||
);
|
||||
await box.put(entry.id, entry);
|
||||
}
|
||||
|
||||
// Add random ovulation symptoms near day 14
|
||||
final ovulationDay = cycleStart.add(const Duration(days: 14));
|
||||
if (ovulationDay.isBefore(today)) {
|
||||
final entry = CycleEntry(
|
||||
id: const Uuid().v4(),
|
||||
date: ovulationDay,
|
||||
isPeriodDay: false,
|
||||
energyLevel: 4, // High energy
|
||||
mood: MoodLevel.veryHappy,
|
||||
createdAt: ovulationDay,
|
||||
updatedAt: ovulationDay,
|
||||
);
|
||||
await box.put(entry.id, entry);
|
||||
}
|
||||
|
||||
cycleStart = cycleStart.add(const Duration(days: 28));
|
||||
}
|
||||
|
||||
_loadEntries();
|
||||
}
|
||||
}
|
||||
|
||||
/// Computed provider for current cycle info
|
||||
|
||||
@@ -326,7 +326,11 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? Colors.white : Colors.transparent,
|
||||
color: isSelected
|
||||
? (Theme.of(context).brightness == Brightness.dark
|
||||
? const Color(0xFF333333)
|
||||
: Colors.white)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: isSelected
|
||||
? [
|
||||
@@ -396,7 +400,7 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.charcoal,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
@@ -429,7 +433,7 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
label,
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
color: AppColors.charcoal,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -637,8 +641,8 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Did you use pantyliners today?',
|
||||
style:
|
||||
GoogleFonts.outfit(fontSize: 14, color: AppColors.charcoal),
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 14, color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
|
||||
@@ -143,7 +143,7 @@ class _DevotionalScreenState extends ConsumerState<DevotionalScreen> {
|
||||
phase.description,
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
color: AppColors.warmGray,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
@@ -170,7 +170,7 @@ class _DevotionalScreenState extends ConsumerState<DevotionalScreen> {
|
||||
onPressed: () => ref
|
||||
.read(scriptureProvider.notifier)
|
||||
.getPreviousScripture(),
|
||||
color: AppColors.charcoal,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
@@ -180,7 +180,7 @@ class _DevotionalScreenState extends ConsumerState<DevotionalScreen> {
|
||||
onPressed: () => ref
|
||||
.read(scriptureProvider.notifier)
|
||||
.getNextScripture(),
|
||||
color: AppColors.charcoal,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -321,16 +321,16 @@ class _DevotionalScreenState extends ConsumerState<DevotionalScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
Row(
|
||||
children: [
|
||||
Text('🙏', style: TextStyle(fontSize: 20)),
|
||||
SizedBox(width: 8),
|
||||
const Text('🙏', style: TextStyle(fontSize: 20)),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Prayer Prompt',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.charcoal, // Assuming a default color
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -460,7 +460,7 @@ class _DevotionalScreenState extends ConsumerState<DevotionalScreen> {
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.gold.withValues(alpha: 0.5)),
|
||||
boxShadow: [
|
||||
@@ -510,7 +510,7 @@ class _DevotionalScreenState extends ConsumerState<DevotionalScreen> {
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.charcoal,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
if (latestPlan.scriptureReference.isNotEmpty) ...[
|
||||
@@ -530,7 +530,7 @@ class _DevotionalScreenState extends ConsumerState<DevotionalScreen> {
|
||||
style: GoogleFonts.lora(
|
||||
fontSize: 15,
|
||||
height: 1.5,
|
||||
color: AppColors.charcoal.withValues(alpha: 0.9),
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -543,7 +543,7 @@ class _DevotionalScreenState extends ConsumerState<DevotionalScreen> {
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: AppColors.warmGray.withValues(alpha: 0.3),
|
||||
@@ -595,7 +595,10 @@ class _DevotionalScreenState extends ConsumerState<DevotionalScreen> {
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.charcoal.withValues(alpha: 0.7),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
@@ -245,7 +245,7 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
|
||||
onPressed: () => ref
|
||||
.read(scriptureProvider.notifier)
|
||||
.getPreviousScripture(),
|
||||
color: AppColors.charcoal,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
@@ -255,7 +255,7 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> {
|
||||
onPressed: () => ref
|
||||
.read(scriptureProvider.notifier)
|
||||
.getNextScripture(),
|
||||
color: AppColors.charcoal,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -329,6 +329,8 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> {
|
||||
Widget build(BuildContext context) {
|
||||
final user = ref.watch(userProfileProvider);
|
||||
final cycleInfo = ref.watch(currentCycleInfoProvider);
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
final wifeName = user?.partnerName ?? "Wife";
|
||||
final phase = cycleInfo.phase;
|
||||
@@ -349,7 +351,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> {
|
||||
'Hey there,',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 16,
|
||||
color: AppColors.warmGray,
|
||||
color: isDark ? Colors.white70 : AppColors.warmGray,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
@@ -357,7 +359,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> {
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.navyBlue,
|
||||
color: isDark ? Colors.white : AppColors.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -460,11 +462,12 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> {
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: isDark ? const Color(0xFF1E1E1E) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.navyBlue.withValues(alpha: 0.05),
|
||||
color: (isDark ? Colors.black : AppColors.navyBlue)
|
||||
.withValues(alpha: 0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
@@ -494,7 +497,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> {
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.navyBlue,
|
||||
color: isDark ? Colors.white : AppColors.navyBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -504,7 +507,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> {
|
||||
_getSupportTip(phase),
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
color: AppColors.charcoal,
|
||||
color: isDark ? Colors.white70 : AppColors.charcoal,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -538,7 +541,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> {
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: isDark ? const Color(0xFF1E1E1E) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: AppColors.rose.withValues(alpha: 0.3)),
|
||||
@@ -574,7 +577,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> {
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.navyBlue,
|
||||
color: isDark ? Colors.white : AppColors.navyBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -589,7 +592,9 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> {
|
||||
backgroundColor:
|
||||
AppColors.rose.withValues(alpha: 0.1),
|
||||
labelStyle: GoogleFonts.outfit(
|
||||
color: AppColors.navyBlue,
|
||||
color: isDark
|
||||
? Colors.white
|
||||
: AppColors.navyBlue,
|
||||
fontWeight: FontWeight.w500),
|
||||
side: BorderSide.none,
|
||||
))
|
||||
@@ -636,7 +641,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> {
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.warmGray,
|
||||
color: isDark ? Colors.white70 : AppColors.warmGray,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -676,7 +681,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> {
|
||||
style: GoogleFonts.lora(
|
||||
fontSize: 15,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: AppColors.navyBlue,
|
||||
color: isDark ? Colors.white : AppColors.navyBlue,
|
||||
height: 1.6,
|
||||
),
|
||||
),
|
||||
@@ -689,7 +694,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> {
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.warmGray,
|
||||
color: isDark ? Colors.white70 : AppColors.warmGray,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
@@ -737,8 +742,9 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> {
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.w500),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.navyBlue,
|
||||
side: const BorderSide(color: AppColors.navyBlue),
|
||||
foregroundColor: isDark ? Colors.white : AppColors.navyBlue,
|
||||
side: BorderSide(
|
||||
color: isDark ? Colors.white70 : AppColors.navyBlue),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
),
|
||||
@@ -854,7 +860,7 @@ class _HusbandDashboardState extends ConsumerState<_HusbandDashboard> {
|
||||
style: GoogleFonts.lora(
|
||||
fontSize: 16,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: AppColors.charcoal,
|
||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
height: 1.6,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -108,7 +108,7 @@ class HusbandSettingsScreen extends ConsumerWidget {
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.navyBlue,
|
||||
color: Theme.of(context).textTheme.titleLarge?.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -147,11 +147,11 @@ class HusbandSettingsScreen extends ConsumerWidget {
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
title: const Row(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.link, color: AppColors.navyBlue),
|
||||
SizedBox(width: 8),
|
||||
Text('Connect with Wife'),
|
||||
Icon(Icons.link, color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Connect with Wife'),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
@@ -159,8 +159,10 @@ class HusbandSettingsScreen extends ConsumerWidget {
|
||||
children: [
|
||||
Text(
|
||||
'Enter the pairing code from your wife\'s app:',
|
||||
style:
|
||||
GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray),
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
@@ -174,8 +176,10 @@ class HusbandSettingsScreen extends ConsumerWidget {
|
||||
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),
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
|
||||
@@ -23,8 +23,8 @@ class LogScreen extends ConsumerStatefulWidget {
|
||||
class _LogScreenState extends ConsumerState<LogScreen> {
|
||||
late DateTime _selectedDate;
|
||||
String? _existingEntryId;
|
||||
bool _isPeriodDay = false;
|
||||
bool _isSpotting = false;
|
||||
bool? _isPeriodDay;
|
||||
bool? _isSpotting;
|
||||
FlowIntensity? _flowIntensity;
|
||||
MoodLevel? _mood;
|
||||
int? _energyLevel;
|
||||
@@ -45,12 +45,12 @@ class _LogScreenState extends ConsumerState<LogScreen> {
|
||||
TextEditingController();
|
||||
|
||||
// Intimacy tracking
|
||||
bool _hadIntimacy = false;
|
||||
bool? _hadIntimacy;
|
||||
bool?
|
||||
_intimacyProtected; // null = no selection, true = protected, false = unprotected
|
||||
|
||||
// Pantyliner / Supply tracking
|
||||
bool _usedPantyliner = false; // Used for "Did you use supplies?"
|
||||
bool? _usedPantyliner; // Used for "Did you use supplies?"
|
||||
int _pantylinerCount = 0;
|
||||
int? _selectedSupplyIndex; // Index of selected supply from inventory
|
||||
|
||||
@@ -126,10 +126,10 @@ class _LogScreenState extends ConsumerState<LogScreen> {
|
||||
final entry = CycleEntry(
|
||||
id: _existingEntryId ?? const Uuid().v4(),
|
||||
date: _selectedDate,
|
||||
isPeriodDay: _isPeriodDay,
|
||||
flowIntensity: _isPeriodDay
|
||||
isPeriodDay: _isPeriodDay ?? false,
|
||||
flowIntensity: _isPeriodDay == true
|
||||
? _flowIntensity
|
||||
: (_isSpotting ? FlowIntensity.spotting : null),
|
||||
: (_isSpotting == true ? FlowIntensity.spotting : null),
|
||||
mood: _mood,
|
||||
energyLevel: _energyLevel,
|
||||
crampIntensity: _crampIntensity > 0 ? _crampIntensity : null,
|
||||
@@ -146,10 +146,10 @@ class _LogScreenState extends ConsumerState<LogScreen> {
|
||||
notes: _notesController.text.isNotEmpty ? _notesController.text : null,
|
||||
cravings: cravings,
|
||||
husbandNotes: _husbandNotes,
|
||||
hadIntimacy: _hadIntimacy,
|
||||
intimacyProtected: _hadIntimacy ? _intimacyProtected : null,
|
||||
usedPantyliner: _usedPantyliner,
|
||||
pantylinerCount: _usedPantyliner ? _pantylinerCount : 0,
|
||||
hadIntimacy: _hadIntimacy ?? false,
|
||||
intimacyProtected: _hadIntimacy == true ? _intimacyProtected : null,
|
||||
usedPantyliner: _usedPantyliner ?? false,
|
||||
pantylinerCount: _usedPantyliner == true ? _pantylinerCount : 0,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
@@ -161,7 +161,7 @@ class _LogScreenState extends ConsumerState<LogScreen> {
|
||||
}
|
||||
|
||||
// Trigger Notification if Period Start
|
||||
if (_isPeriodDay &&
|
||||
if (_isPeriodDay == true &&
|
||||
ref.read(userProfileProvider)?.notifyPeriodStart == true) {
|
||||
final entries = ref.read(cycleEntriesProvider);
|
||||
final yesterday = _selectedDate.subtract(const Duration(days: 1));
|
||||
@@ -198,8 +198,8 @@ class _LogScreenState extends ConsumerState<LogScreen> {
|
||||
void _resetForm() {
|
||||
setState(() {
|
||||
_existingEntryId = null;
|
||||
_isPeriodDay = false;
|
||||
_isSpotting = false;
|
||||
_isPeriodDay = null;
|
||||
_isSpotting = null;
|
||||
_flowIntensity = null;
|
||||
_mood = null;
|
||||
_energyLevel = 3;
|
||||
@@ -217,9 +217,9 @@ class _LogScreenState extends ConsumerState<LogScreen> {
|
||||
_notesController.clear();
|
||||
_cravingsController.clear();
|
||||
_husbandNotes = null;
|
||||
_hadIntimacy = false;
|
||||
_hadIntimacy = null;
|
||||
_intimacyProtected = null;
|
||||
_usedPantyliner = false;
|
||||
_usedPantyliner = null;
|
||||
_pantylinerCount = 0;
|
||||
});
|
||||
}
|
||||
@@ -321,7 +321,7 @@ class _LogScreenState extends ConsumerState<LogScreen> {
|
||||
),
|
||||
|
||||
// Are you spotting? (only if NOT period day)
|
||||
if (!_isPeriodDay) ...[
|
||||
if (_isPeriodDay != true) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildSectionCard(
|
||||
context,
|
||||
@@ -350,7 +350,8 @@ class _LogScreenState extends ConsumerState<LogScreen> {
|
||||
],
|
||||
|
||||
// Still on Period? (If predicted but toggle is NO)
|
||||
if (!_isPeriodDay && _shouldShowPeriodCompletionPrompt()) ...[
|
||||
if (_isPeriodDay == false &&
|
||||
_shouldShowPeriodCompletionPrompt()) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildSectionCard(
|
||||
context,
|
||||
@@ -406,7 +407,7 @@ class _LogScreenState extends ConsumerState<LogScreen> {
|
||||
],
|
||||
|
||||
// Flow Intensity (only if period day)
|
||||
if (_isPeriodDay) ...[
|
||||
if (_isPeriodDay == true) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildSectionCard(
|
||||
context,
|
||||
@@ -474,7 +475,7 @@ class _LogScreenState extends ConsumerState<LogScreen> {
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PadTrackerScreen(
|
||||
isSpotting: _isSpotting,
|
||||
isSpotting: _isSpotting ?? false,
|
||||
initialFlow: _flowIntensity,
|
||||
),
|
||||
),
|
||||
@@ -515,7 +516,7 @@ class _LogScreenState extends ConsumerState<LogScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_usedPantyliner) ...[
|
||||
if (_usedPantyliner == true) ...[
|
||||
const SizedBox(height: 12),
|
||||
if (userProfile?.padSupplies?.isNotEmpty == true) ...[
|
||||
Text(
|
||||
@@ -844,15 +845,17 @@ class _LogScreenState extends ConsumerState<LogScreen> {
|
||||
SwitchListTile(
|
||||
title: Text('Had Intimacy Today',
|
||||
style: GoogleFonts.outfit(fontSize: 14)),
|
||||
value: _hadIntimacy,
|
||||
value: _hadIntimacy ?? false,
|
||||
onChanged: (val) => setState(() {
|
||||
_hadIntimacy = val;
|
||||
if (!val) _intimacyProtected = null;
|
||||
if (!val) {
|
||||
_intimacyProtected = null;
|
||||
}
|
||||
}),
|
||||
activeThumbColor: AppColors.sageGreen,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
if (_hadIntimacy) ...[
|
||||
if (_hadIntimacy == true) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text('Protection:',
|
||||
style: GoogleFonts.outfit(
|
||||
@@ -1022,7 +1025,7 @@ class _LogScreenState extends ConsumerState<LogScreen> {
|
||||
}
|
||||
|
||||
Widget _buildYesNoControl(BuildContext context,
|
||||
{required bool value,
|
||||
{required bool? value,
|
||||
required ValueChanged<bool> onChanged,
|
||||
required Color activeColor}) {
|
||||
final theme = Theme.of(context);
|
||||
@@ -1037,24 +1040,24 @@ class _LogScreenState extends ConsumerState<LogScreen> {
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: !value
|
||||
color: value == false
|
||||
? theme.colorScheme.error
|
||||
.withValues(alpha: isDark ? 0.3 : 0.2)
|
||||
: theme.colorScheme.surfaceContainerHighest
|
||||
.withValues(alpha: 0.3),
|
||||
borderRadius:
|
||||
const BorderRadius.horizontal(left: Radius.circular(8)),
|
||||
border: !value
|
||||
border: value == false
|
||||
? Border.all(color: theme.colorScheme.error)
|
||||
: Border.all(color: Colors.transparent),
|
||||
),
|
||||
child: Text(
|
||||
'No',
|
||||
style: GoogleFonts.outfit(
|
||||
color: !value
|
||||
color: value == false
|
||||
? theme.colorScheme.error
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
fontWeight: !value ? FontWeight.w600 : FontWeight.w400,
|
||||
fontWeight: value == false ? FontWeight.w600 : FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1065,21 +1068,23 @@ class _LogScreenState extends ConsumerState<LogScreen> {
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: value
|
||||
color: value == true
|
||||
? activeColor.withValues(alpha: isDark ? 0.3 : 0.2)
|
||||
: theme.colorScheme.surfaceContainerHighest
|
||||
.withValues(alpha: 0.3),
|
||||
borderRadius:
|
||||
const BorderRadius.horizontal(right: Radius.circular(8)),
|
||||
border: value
|
||||
border: value == true
|
||||
? Border.all(color: activeColor)
|
||||
: Border.all(color: Colors.transparent),
|
||||
),
|
||||
child: Text(
|
||||
'Yes',
|
||||
style: GoogleFonts.outfit(
|
||||
color: value ? activeColor : theme.colorScheme.onSurfaceVariant,
|
||||
fontWeight: value ? FontWeight.w600 : FontWeight.w400,
|
||||
color: value == true
|
||||
? activeColor
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
fontWeight: value == true ? FontWeight.w600 : FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -28,6 +28,7 @@ class _PadTrackerScreenState extends ConsumerState<PadTrackerScreen> {
|
||||
Timer? _timer;
|
||||
Duration _timeSinceLastChange = Duration.zero;
|
||||
int? _activeSupplyIndex;
|
||||
SupplyItem? _manualSupply;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -81,67 +82,78 @@ class _PadTrackerScreenState extends ConsumerState<PadTrackerScreen> {
|
||||
lastChange.day == now.day;
|
||||
|
||||
if (!changedToday) {
|
||||
await showDialog(
|
||||
final result = await showDialog<_PadLogResult>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Track Your Change',
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.bold)),
|
||||
content: Text(
|
||||
'When did you last change your pad/tampon?',
|
||||
style: GoogleFonts.outfit(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_updateLastChangeTime(DateTime.now());
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Just Now'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final time = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.now(),
|
||||
);
|
||||
if (time != null && context.mounted) {
|
||||
final now = DateTime.now();
|
||||
final selectedDate = DateTime(
|
||||
now.year, now.month, now.day, time.hour, time.minute);
|
||||
if (selectedDate.isAfter(now)) {
|
||||
_updateLastChangeTime(now);
|
||||
} else {
|
||||
_updateLastChangeTime(selectedDate);
|
||||
}
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: const Text('Pick Time'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Skip'),
|
||||
),
|
||||
],
|
||||
),
|
||||
builder: (context) => const _PadCheckInDialog(),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
if (result.skipped) return;
|
||||
|
||||
_finalizeLog(
|
||||
result.time,
|
||||
result.flow,
|
||||
supply: result.supply,
|
||||
supplyIndex: result.supplyIndex,
|
||||
deductInventory: result.deductInventory,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateLastChangeTime(DateTime time) async {
|
||||
Future<void> _finalizeLog(DateTime time, FlowIntensity flow,
|
||||
{required SupplyItem supply,
|
||||
required int? supplyIndex,
|
||||
required bool deductInventory}) async {
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user != null) {
|
||||
final updatedProfile = user.copyWith(
|
||||
UserProfile updatedProfile = user;
|
||||
|
||||
// Deduct inventory if needed
|
||||
if (deductInventory &&
|
||||
user.isAutoInventoryEnabled &&
|
||||
supplyIndex != null) {
|
||||
// Clone the supplies list
|
||||
List<SupplyItem> newSupplies = List.from(user.padSupplies ?? []);
|
||||
if (supplyIndex < newSupplies.length) {
|
||||
final oldItem = newSupplies[supplyIndex];
|
||||
if (oldItem.count > 0) {
|
||||
newSupplies[supplyIndex] =
|
||||
oldItem.copyWith(count: oldItem.count - 1);
|
||||
}
|
||||
}
|
||||
updatedProfile = updatedProfile.copyWith(padSupplies: newSupplies);
|
||||
}
|
||||
|
||||
// Update Last Change Time
|
||||
updatedProfile = updatedProfile.copyWith(
|
||||
lastPadChangeTime: time,
|
||||
lastInventoryUpdate:
|
||||
deductInventory ? DateTime.now() : user.lastInventoryUpdate,
|
||||
);
|
||||
|
||||
await ref
|
||||
.read(userProfileProvider.notifier)
|
||||
.updateProfile(updatedProfile);
|
||||
|
||||
setState(() {
|
||||
_activeSupplyIndex = supplyIndex;
|
||||
if (supplyIndex == null) {
|
||||
_manualSupply = supply;
|
||||
} else {
|
||||
_manualSupply = null;
|
||||
}
|
||||
});
|
||||
|
||||
_updateTimeSinceChange();
|
||||
_scheduleReminders(time);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Logged! Timer started.')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,6 +222,8 @@ class _PadTrackerScreenState extends ConsumerState<PadTrackerScreen> {
|
||||
}
|
||||
|
||||
SupplyItem? get _activeSupply {
|
||||
if (_manualSupply != null) return _manualSupply;
|
||||
|
||||
final user = ref.watch(userProfileProvider);
|
||||
if (user == null || user.padSupplies == null || user.padSupplies!.isEmpty) {
|
||||
return null;
|
||||
@@ -943,3 +957,307 @@ class _SupplyManagementPopupState
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PadLogResult {
|
||||
final DateTime time;
|
||||
final FlowIntensity flow;
|
||||
final SupplyItem supply;
|
||||
final int? supplyIndex;
|
||||
final bool deductInventory;
|
||||
final bool skipped;
|
||||
|
||||
_PadLogResult({
|
||||
required this.time,
|
||||
required this.flow,
|
||||
required this.supply,
|
||||
required this.supplyIndex,
|
||||
required this.deductInventory,
|
||||
this.skipped = false,
|
||||
});
|
||||
|
||||
factory _PadLogResult.skipped() {
|
||||
return _PadLogResult(
|
||||
time: DateTime.now(),
|
||||
flow: FlowIntensity.medium,
|
||||
supply:
|
||||
SupplyItem(brand: '', type: PadType.regular, absorbency: 0, count: 0),
|
||||
supplyIndex: null,
|
||||
deductInventory: false,
|
||||
skipped: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PadCheckInDialog extends ConsumerStatefulWidget {
|
||||
const _PadCheckInDialog();
|
||||
|
||||
@override
|
||||
ConsumerState<_PadCheckInDialog> createState() => _PadCheckInDialogState();
|
||||
}
|
||||
|
||||
class _PadCheckInDialogState extends ConsumerState<_PadCheckInDialog> {
|
||||
DateTime _selectedTime = DateTime.now(); // "Just Now" by default
|
||||
FlowIntensity _selectedFlow = FlowIntensity.medium;
|
||||
int? _selectedSupplyIndex;
|
||||
SupplyItem? _selectedSupply;
|
||||
bool _useBorrowed = false;
|
||||
PadType _borrowedType = PadType.regular;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Default to first available supply if exists
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user != null &&
|
||||
user.padSupplies != null &&
|
||||
user.padSupplies!.isNotEmpty) {
|
||||
setState(() {
|
||||
_selectedSupplyIndex = 0;
|
||||
_selectedSupply = user.padSupplies![0];
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_useBorrowed = true; // Fallback if no inventory
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onTimePressed() async {
|
||||
final TimeOfDay? picked = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.fromDateTime(_selectedTime),
|
||||
);
|
||||
if (picked != null) {
|
||||
final now = DateTime.now();
|
||||
setState(() {
|
||||
_selectedTime = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
picked.hour,
|
||||
picked.minute,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String _formatTimeDisplay() {
|
||||
final diff = DateTime.now().difference(_selectedTime).abs();
|
||||
if (diff.inMinutes < 1) return 'Just Now';
|
||||
// Format H:mm AM/PM would be better, but keeping simple 24h or simple format
|
||||
// Just using H:mm for now as in code snippet
|
||||
final hour = _selectedTime.hour > 12
|
||||
? _selectedTime.hour - 12
|
||||
: (_selectedTime.hour == 0 ? 12 : _selectedTime.hour);
|
||||
final period = _selectedTime.hour >= 12 ? 'PM' : 'AM';
|
||||
return '$hour:${_selectedTime.minute.toString().padLeft(2, '0')} $period';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final user = ref.watch(userProfileProvider);
|
||||
final supplies = user?.padSupplies ?? [];
|
||||
|
||||
return AlertDialog(
|
||||
title: Text('Track Your Change',
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.bold)),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 1. Time Selection
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.access_time,
|
||||
size: 20, color: AppColors.warmGray),
|
||||
const SizedBox(width: 8),
|
||||
Text('Time: ',
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.w600)),
|
||||
Text(_formatTimeDisplay(), style: GoogleFonts.outfit()),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
onPressed: _onTimePressed,
|
||||
child: const Text('Edit'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
|
||||
// 2. Flow Intensity
|
||||
Text('Flow Intensity:',
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: FlowIntensity.values.map((f) {
|
||||
return ChoiceChip(
|
||||
label: Text(f.label),
|
||||
selected: _selectedFlow == f,
|
||||
onSelected: (selected) {
|
||||
if (selected) setState(() => _selectedFlow = f);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 3. Supply Selection
|
||||
Text('Supply:',
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
if (supplies.isNotEmpty) ...[
|
||||
// Inventory Dropdown
|
||||
RadioListTile<bool>(
|
||||
title: const Text('Use Inventory'),
|
||||
value: false, // _useBorrowed = false
|
||||
groupValue: _useBorrowed,
|
||||
onChanged: (val) => setState(() => _useBorrowed = false),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
if (!_useBorrowed)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0, bottom: 8),
|
||||
child: DropdownButtonFormField<int>(
|
||||
isExpanded: true,
|
||||
initialValue: _selectedSupplyIndex,
|
||||
items: supplies.asMap().entries.map((entry) {
|
||||
final s = entry.value;
|
||||
return DropdownMenuItem(
|
||||
value: entry.key,
|
||||
child: Text('${s.brand} ${s.type.label} (x${s.count})',
|
||||
overflow: TextOverflow.ellipsis),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_selectedSupplyIndex = val;
|
||||
if (val != null) _selectedSupply = supplies[val];
|
||||
});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// Borrowed / Other
|
||||
RadioListTile<bool>(
|
||||
title: const Text('Borrowed / Other'),
|
||||
value: true, // _useBorrowed = true
|
||||
groupValue: _useBorrowed,
|
||||
onChanged: (val) => setState(() => _useBorrowed = true),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
|
||||
if (_useBorrowed)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: DropdownButtonFormField<PadType>(
|
||||
isExpanded: true,
|
||||
value: _borrowedType,
|
||||
items: PadType.values.map((t) {
|
||||
return DropdownMenuItem(
|
||||
value: t,
|
||||
child: Text(t.label),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (val) {
|
||||
if (val != null) setState(() => _borrowedType = val);
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, _PadLogResult.skipped());
|
||||
},
|
||||
child: const Text('Skip'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
SupplyItem supplyToUse;
|
||||
bool deduct = false;
|
||||
int? index;
|
||||
|
||||
if (_useBorrowed) {
|
||||
// Create temp supply
|
||||
int absorbency = 3;
|
||||
switch (_borrowedType) {
|
||||
case PadType.pantyLiner:
|
||||
absorbency = 1;
|
||||
break;
|
||||
case PadType.regular:
|
||||
case PadType.tamponRegular:
|
||||
absorbency = 3;
|
||||
break;
|
||||
case PadType.superPad:
|
||||
case PadType.tamponSuper:
|
||||
case PadType.overnight:
|
||||
absorbency = 5;
|
||||
break;
|
||||
default:
|
||||
absorbency = 4;
|
||||
}
|
||||
supplyToUse = SupplyItem(
|
||||
brand: 'Borrowed',
|
||||
type: _borrowedType,
|
||||
absorbency: absorbency,
|
||||
count: 0);
|
||||
deduct = false;
|
||||
index = null;
|
||||
} else {
|
||||
if (_selectedSupply == null) {
|
||||
// Auto-select first if available as fallback
|
||||
if (supplies.isNotEmpty) {
|
||||
supplyToUse = supplies.first;
|
||||
index = 0;
|
||||
deduct = true;
|
||||
} else {
|
||||
supplyToUse = SupplyItem(
|
||||
brand: 'Generic',
|
||||
type: PadType.regular,
|
||||
absorbency: 3,
|
||||
count: 0);
|
||||
index = null;
|
||||
deduct = false;
|
||||
}
|
||||
} else {
|
||||
supplyToUse = _selectedSupply!;
|
||||
deduct = true;
|
||||
index = _selectedSupplyIndex;
|
||||
}
|
||||
}
|
||||
|
||||
Navigator.pop(
|
||||
context,
|
||||
_PadLogResult(
|
||||
time: _selectedTime,
|
||||
flow: _selectedFlow,
|
||||
supply: supplyToUse,
|
||||
supplyIndex: index,
|
||||
deductInventory: deduct,
|
||||
));
|
||||
},
|
||||
child: const Text('Log Change'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,12 +142,20 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
isIrregularCycle: _isIrregularCycle,
|
||||
hasCompletedOnboarding: true,
|
||||
useExampleData: _useExampleData,
|
||||
isPadTrackingEnabled: _isPadTrackingEnabled,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(userProfile);
|
||||
|
||||
// Generate example data if requested
|
||||
if (_useExampleData) {
|
||||
await ref
|
||||
.read(cycleEntriesProvider.notifier)
|
||||
.generateExampleData(userProfile.id);
|
||||
}
|
||||
|
||||
// Trigger partner connection notification if applicable
|
||||
if (!_skipPartnerConnection && !_useExampleData) {
|
||||
await NotificationService().showPartnerUpdateNotification(
|
||||
|
||||
@@ -133,7 +133,7 @@ class _SuppliesSettingsScreenState
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.charcoal,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -278,7 +278,8 @@ class _SuppliesSettingsScreenState
|
||||
title: Text(
|
||||
'Auto-deduct on Log',
|
||||
style: GoogleFonts.outfit(
|
||||
fontWeight: FontWeight.w500, color: AppColors.charcoal),
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Reduce count when you log a pad',
|
||||
@@ -308,7 +309,8 @@ class _SuppliesSettingsScreenState
|
||||
title: Text(
|
||||
'Show Minutes',
|
||||
style: GoogleFonts.outfit(
|
||||
fontWeight: FontWeight.w500, color: AppColors.charcoal),
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
value: _showPadTimerMinutes,
|
||||
onChanged: (val) {
|
||||
|
||||
@@ -157,26 +157,41 @@ class CycleService {
|
||||
}
|
||||
|
||||
// Handle cases where last period was long ago (more than one cycle)
|
||||
final dayOfCycle = ((daysSinceLastPeriod - 1) % cycleLength) + 1;
|
||||
final daysUntilPeriod = cycleLength - dayOfCycle;
|
||||
final int calculatedDayOfCycle =
|
||||
((daysSinceLastPeriod - 1) % cycleLength) + 1;
|
||||
|
||||
// Check if we are in the predicted menstrual phase but no period is logged
|
||||
bool isPeriodLoggedToday =
|
||||
entries.any((e) => DateUtils.isSameDay(e.date, now) && e.isPeriodDay);
|
||||
|
||||
CyclePhase phase;
|
||||
if (dayOfCycle <= user.averagePeriodLength) {
|
||||
// Use variable period length
|
||||
phase = CyclePhase.menstrual;
|
||||
} else if (dayOfCycle <= 13) {
|
||||
int dayOfCycle = calculatedDayOfCycle;
|
||||
|
||||
if (calculatedDayOfCycle <= user.averagePeriodLength) {
|
||||
if (isPeriodLoggedToday) {
|
||||
phase = CyclePhase.menstrual;
|
||||
} else {
|
||||
// No period logged today, but we are in the predicted window.
|
||||
// Stay in Luteal and extend the day count.
|
||||
phase = CyclePhase.luteal;
|
||||
dayOfCycle = daysSinceLastPeriod;
|
||||
}
|
||||
} else if (calculatedDayOfCycle <= 13) {
|
||||
phase = CyclePhase.follicular;
|
||||
} else if (dayOfCycle <= 16) {
|
||||
} else if (calculatedDayOfCycle <= 16) {
|
||||
phase = CyclePhase.ovulation;
|
||||
} else {
|
||||
phase = CyclePhase.luteal;
|
||||
}
|
||||
|
||||
final daysUntilPeriod =
|
||||
dayOfCycle >= cycleLength ? 0 : cycleLength - dayOfCycle;
|
||||
|
||||
return CycleInfo(
|
||||
phase: phase,
|
||||
dayOfCycle: dayOfCycle,
|
||||
daysUntilPeriod: daysUntilPeriod,
|
||||
isPeriodExpected: daysUntilPeriod <= 0 || dayOfCycle <= 5,
|
||||
isPeriodExpected: daysUntilPeriod <= 0 || calculatedDayOfCycle <= 5,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ class _PadSettingsDialogState extends ConsumerState<PadSettingsDialog> {
|
||||
'Enable Pad Tracking',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 16,
|
||||
color: AppColors.charcoal,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -359,7 +359,8 @@ class _PadSettingsDialogState extends ConsumerState<PadSettingsDialog> {
|
||||
title: Text(
|
||||
'Auto-deduct on Log',
|
||||
style: GoogleFonts.outfit(
|
||||
fontWeight: FontWeight.w500, color: AppColors.charcoal),
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Reduce count when you log a pad',
|
||||
|
||||
@@ -140,6 +140,8 @@ class _PadTrackerCardState extends ConsumerState<PadTrackerCard> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
@@ -150,7 +152,7 @@ class _PadTrackerCardState extends ConsumerState<PadTrackerCard> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: isDark ? const Color(0xFF1E1E1E) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: _statusColor.withValues(alpha: 0.3)),
|
||||
boxShadow: [
|
||||
@@ -187,7 +189,7 @@ class _PadTrackerCardState extends ConsumerState<PadTrackerCard> {
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.charcoal,
|
||||
color: isDark ? Colors.white : AppColors.charcoal,
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
@@ -202,7 +204,9 @@ class _PadTrackerCardState extends ConsumerState<PadTrackerCard> {
|
||||
? Icons.arrow_downward
|
||||
: Icons.arrow_upward,
|
||||
size: 16,
|
||||
color: AppColors.warmGray),
|
||||
color: isDark
|
||||
? Colors.white70
|
||||
: AppColors.warmGray),
|
||||
)
|
||||
],
|
||||
),
|
||||
@@ -217,7 +221,8 @@ class _PadTrackerCardState extends ConsumerState<PadTrackerCard> {
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right, color: AppColors.lightGray),
|
||||
Icon(Icons.chevron_right,
|
||||
color: isDark ? Colors.white24 : AppColors.lightGray),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -91,8 +91,8 @@ class QuickLogButtons extends ConsumerWidget {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return SizedBox(
|
||||
height: 80,
|
||||
width: 75,
|
||||
// Removed fixed height to prevent overflow on larger text scalings
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
@@ -118,7 +118,9 @@ class QuickLogButtons extends ConsumerWidget {
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 12, // Slightly larger text
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isDark ? Colors.white.withValues(alpha: 0.9) : color,
|
||||
color: isDark
|
||||
? Colors.white.withValues(alpha: 0.9)
|
||||
: AppColors.charcoal,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -4,10 +4,12 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/cycle_entry.dart';
|
||||
import '../models/user_profile.dart';
|
||||
import '../providers/user_provider.dart';
|
||||
import '../providers/navigation_provider.dart';
|
||||
import '../screens/log/pad_tracker_screen.dart';
|
||||
import '../services/notification_service.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
class QuickLogDialog extends ConsumerStatefulWidget {
|
||||
final String logType;
|
||||
@@ -23,6 +25,8 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
|
||||
FlowIntensity? _flowIntensity;
|
||||
MoodLevel? _mood;
|
||||
int? _energyLevel;
|
||||
PadType? _selectedPadType;
|
||||
int _padAbsorbency = 3;
|
||||
|
||||
// Symptoms & Cravings
|
||||
final Map<String, bool> _symptoms = {
|
||||
@@ -274,18 +278,138 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
|
||||
}
|
||||
|
||||
Widget _buildPadsLog() {
|
||||
// This can be a simple button to navigate to the PadTrackerScreen
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => const PadTrackerScreen(),
|
||||
));
|
||||
},
|
||||
child: const Text('Track Pad Change'),
|
||||
final theme = Theme.of(context);
|
||||
// ignore: unused_local_variable
|
||||
final user = ref.watch(userProfileProvider);
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Flow Intensity:',
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: FlowIntensity.values.map((flow) {
|
||||
return ChoiceChip(
|
||||
label: Text(flow.label),
|
||||
selected: _flowIntensity == flow,
|
||||
onSelected: (selected) {
|
||||
if (selected) setState(() => _flowIntensity = flow);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('Protection Type:',
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<PadType>(
|
||||
initialValue: _selectedPadType,
|
||||
decoration: InputDecoration(
|
||||
border:
|
||||
OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
),
|
||||
items: PadType.values.map((type) {
|
||||
return DropdownMenuItem(
|
||||
value: type,
|
||||
child: Text(type.label),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) => setState(() => _selectedPadType = value),
|
||||
hint: const Text('Select type'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('Absorbency (1-5):',
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.w600)),
|
||||
Slider(
|
||||
value: _padAbsorbency.toDouble(),
|
||||
min: 1,
|
||||
max: 5,
|
||||
divisions: 4,
|
||||
label: _padAbsorbency.toString(),
|
||||
onChanged: (val) => setState(() => _padAbsorbency = val.round()),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (_flowIntensity != null && _selectedPadType != null)
|
||||
Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.sageGreen.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'Estimated duration: ${_calculateRecommendedHours()} hours',
|
||||
style: GoogleFonts.outfit(
|
||||
color: AppColors.sageGreen,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
ref.read(navigationProvider.notifier).setIndex(2);
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => const PadTrackerScreen(),
|
||||
));
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.surfaceContainerHighest,
|
||||
foregroundColor: theme.colorScheme.onSurface,
|
||||
),
|
||||
child: const Text('More Options (Inventory, etc.)'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
int _calculateRecommendedHours() {
|
||||
if (_selectedPadType == null || _flowIntensity == null) return 6;
|
||||
|
||||
final type = _selectedPadType!;
|
||||
if (type == PadType.menstrualCup ||
|
||||
type == PadType.menstrualDisc ||
|
||||
type == PadType.periodUnderwear) {
|
||||
return 12;
|
||||
}
|
||||
|
||||
int baseHours;
|
||||
switch (_flowIntensity!) {
|
||||
case FlowIntensity.heavy:
|
||||
baseHours = (type == PadType.superPad ||
|
||||
type == PadType.overnight ||
|
||||
type == PadType.tamponSuper)
|
||||
? 4
|
||||
: 3;
|
||||
break;
|
||||
case FlowIntensity.medium:
|
||||
baseHours = 6;
|
||||
break;
|
||||
case FlowIntensity.light:
|
||||
baseHours = 8;
|
||||
break;
|
||||
case FlowIntensity.spotting:
|
||||
baseHours = 10;
|
||||
break;
|
||||
case FlowIntensity.none:
|
||||
baseHours = 8;
|
||||
break;
|
||||
}
|
||||
|
||||
return baseHours;
|
||||
}
|
||||
|
||||
Widget _buildPrayerLog() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -397,8 +521,33 @@ class _QuickLogDialogState extends ConsumerState<QuickLogDialog> {
|
||||
return; // Don't save empty prayer
|
||||
}
|
||||
break;
|
||||
case 'pads':
|
||||
final userProfile = ref.read(userProfileProvider);
|
||||
if (userProfile != null) {
|
||||
final hours = _calculateRecommendedHours();
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(
|
||||
userProfile.copyWith(
|
||||
lastPadChangeTime: DateTime.now(),
|
||||
// Auto-inventory deduction could go here, but omitted for "Quick" simplicity
|
||||
// unless we want to match PadTrackerScreen exactly.
|
||||
),
|
||||
);
|
||||
|
||||
await NotificationService().scheduleNotification(
|
||||
id: 100,
|
||||
title: 'Time to change!',
|
||||
body: 'It\'s been $hours hours since you logged your protection.',
|
||||
scheduledDate: DateTime.now().add(Duration(hours: hours)),
|
||||
);
|
||||
}
|
||||
updatedEntry = entry.copyWith(
|
||||
isPeriodDay: _flowIntensity != FlowIntensity.none &&
|
||||
_flowIntensity != FlowIntensity.spotting,
|
||||
flowIntensity: _flowIntensity,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// pads handled separately
|
||||
// Already handled or invalid
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -128,10 +128,10 @@ class TipCard extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildDetailSection(
|
||||
'Nutrition', details['nutrition']!, Icons.restaurant),
|
||||
context, 'Nutrition', details['nutrition']!, Icons.restaurant),
|
||||
const SizedBox(height: 16),
|
||||
_buildDetailSection(
|
||||
'Movement', details['movement']!, Icons.fitness_center),
|
||||
_buildDetailSection(context, 'Movement', details['movement']!,
|
||||
Icons.fitness_center),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Note: While these are general trends, your body is unique. Always listen to your own energy levels.',
|
||||
@@ -154,7 +154,8 @@ class TipCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailSection(String title, String content, IconData icon) {
|
||||
Widget _buildDetailSection(
|
||||
BuildContext context, String title, String content, IconData icon) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -176,7 +177,7 @@ class TipCard extends StatelessWidget {
|
||||
content,
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 13,
|
||||
color: AppColors.charcoal,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user