1199 lines
45 KiB
Dart
1199 lines
45 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import '../../models/cycle_entry.dart';
|
|
import '../../models/user_profile.dart';
|
|
import '../../providers/navigation_provider.dart';
|
|
import '../../providers/user_provider.dart';
|
|
import '../../theme/app_theme.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
import '../../services/notification_service.dart';
|
|
import 'pad_tracker_screen.dart';
|
|
import '../../services/cycle_service.dart';
|
|
import '../../widgets/protected_wrapper.dart';
|
|
|
|
class LogScreen extends ConsumerStatefulWidget {
|
|
final DateTime? initialDate;
|
|
const LogScreen({super.key, this.initialDate});
|
|
|
|
@override
|
|
ConsumerState<LogScreen> createState() => _LogScreenState();
|
|
}
|
|
|
|
class _LogScreenState extends ConsumerState<LogScreen> {
|
|
late DateTime _selectedDate;
|
|
String? _existingEntryId;
|
|
bool _isPeriodDay = false;
|
|
bool _isSpotting = false;
|
|
FlowIntensity? _flowIntensity;
|
|
MoodLevel? _mood;
|
|
int? _energyLevel;
|
|
int _crampIntensity = 0;
|
|
bool _hasHeadache = false;
|
|
bool _hasBloating = false;
|
|
bool _hasBreastTenderness = false;
|
|
bool _hasFatigue = false;
|
|
bool _hasAcne = false;
|
|
bool _hasLowerBackPain = false;
|
|
bool _hasConstipation = false;
|
|
bool _hasDiarrhea = false;
|
|
bool _hasInsomnia = false;
|
|
int? _stressLevel;
|
|
final TextEditingController _notesController = TextEditingController();
|
|
final TextEditingController _cravingsController = TextEditingController();
|
|
final TextEditingController _pantylinerCountController =
|
|
TextEditingController();
|
|
|
|
// Intimacy tracking
|
|
bool _hadIntimacy = false;
|
|
bool?
|
|
_intimacyProtected; // null = no selection, true = protected, false = unprotected
|
|
|
|
// Pantyliner / Supply tracking
|
|
bool _usedPantyliner = false; // Used for "Did you use supplies?"
|
|
int _pantylinerCount = 0;
|
|
int? _selectedSupplyIndex; // Index of selected supply from inventory
|
|
|
|
// Hidden field to preserve husband's notes
|
|
String? _husbandNotes;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_selectedDate = widget.initialDate ?? DateTime.now();
|
|
|
|
// Defer data loading to avoid build-time ref.read
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_loadExistingData();
|
|
});
|
|
}
|
|
|
|
void _loadExistingData() {
|
|
final entries = ref.read(cycleEntriesProvider);
|
|
try {
|
|
final entry = entries.firstWhere(
|
|
(e) => DateUtils.isSameDay(e.date, _selectedDate),
|
|
);
|
|
setState(() {
|
|
_existingEntryId = entry.id;
|
|
_isPeriodDay = entry.isPeriodDay;
|
|
_isSpotting = entry.flowIntensity == FlowIntensity.spotting;
|
|
_flowIntensity = entry.flowIntensity;
|
|
_mood = entry.mood;
|
|
_energyLevel = entry.energyLevel;
|
|
_crampIntensity = entry.crampIntensity ?? 0;
|
|
_hasHeadache = entry.hasHeadache;
|
|
_hasBloating = entry.hasBloating;
|
|
_hasBreastTenderness = entry.hasBreastTenderness;
|
|
_hasFatigue = entry.hasFatigue;
|
|
_hasAcne = entry.hasAcne;
|
|
_hasLowerBackPain = entry.hasLowerBackPain;
|
|
_hasConstipation = entry.hasConstipation;
|
|
_hasDiarrhea = entry.hasDiarrhea;
|
|
_hasInsomnia = entry.hasInsomnia;
|
|
_stressLevel = entry.stressLevel;
|
|
_notesController.text = entry.notes ?? '';
|
|
_cravingsController.text = entry.cravings?.join(', ') ?? '';
|
|
_husbandNotes = entry.husbandNotes;
|
|
_hadIntimacy = entry.hadIntimacy;
|
|
_intimacyProtected = entry.intimacyProtected;
|
|
_usedPantyliner = entry.usedPantyliner;
|
|
_pantylinerCount = entry.pantylinerCount;
|
|
_pantylinerCountController.text = entry.pantylinerCount.toString();
|
|
});
|
|
} catch (_) {
|
|
// No existing entry for this day
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_notesController.dispose();
|
|
_cravingsController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _saveEntry() async {
|
|
List<String>? cravings;
|
|
if (_cravingsController.text.isNotEmpty) {
|
|
cravings = _cravingsController.text
|
|
.split(',')
|
|
.map((e) => e.trim())
|
|
.where((e) => e.isNotEmpty)
|
|
.toList();
|
|
}
|
|
|
|
final entry = CycleEntry(
|
|
id: _existingEntryId ?? const Uuid().v4(),
|
|
date: _selectedDate,
|
|
isPeriodDay: _isPeriodDay,
|
|
flowIntensity: _isPeriodDay
|
|
? _flowIntensity
|
|
: (_isSpotting ? FlowIntensity.spotting : null),
|
|
mood: _mood,
|
|
energyLevel: _energyLevel,
|
|
crampIntensity: _crampIntensity > 0 ? _crampIntensity : null,
|
|
hasHeadache: _hasHeadache,
|
|
hasBloating: _hasBloating,
|
|
hasBreastTenderness: _hasBreastTenderness,
|
|
hasFatigue: _hasFatigue,
|
|
hasAcne: _hasAcne,
|
|
hasLowerBackPain: _hasLowerBackPain,
|
|
hasConstipation: _hasConstipation,
|
|
hasDiarrhea: _hasDiarrhea,
|
|
hasInsomnia: _hasInsomnia,
|
|
stressLevel: _stressLevel,
|
|
notes: _notesController.text.isNotEmpty ? _notesController.text : null,
|
|
cravings: cravings,
|
|
husbandNotes: _husbandNotes,
|
|
hadIntimacy: _hadIntimacy,
|
|
intimacyProtected: _hadIntimacy ? _intimacyProtected : null,
|
|
usedPantyliner: _usedPantyliner,
|
|
pantylinerCount: _usedPantyliner ? _pantylinerCount : 0,
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
);
|
|
|
|
if (_existingEntryId != null) {
|
|
await ref.read(cycleEntriesProvider.notifier).updateEntry(entry);
|
|
} else {
|
|
await ref.read(cycleEntriesProvider.notifier).addEntry(entry);
|
|
}
|
|
|
|
// Trigger Notification if Period Start
|
|
if (_isPeriodDay &&
|
|
ref.read(userProfileProvider)?.notifyPeriodStart == true) {
|
|
final entries = ref.read(cycleEntriesProvider);
|
|
final yesterday = _selectedDate.subtract(const Duration(days: 1));
|
|
final wasPeriodYesterday = entries
|
|
.any((e) => DateUtils.isSameDay(e.date, yesterday) && e.isPeriodDay);
|
|
|
|
if (!wasPeriodYesterday) {
|
|
NotificationService().showLocalNotification(
|
|
id: 1001,
|
|
title: 'Period Started',
|
|
body: 'Period start recorded for ${_formatDate(_selectedDate)}.',
|
|
);
|
|
}
|
|
}
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Entry saved!', style: GoogleFonts.outfit()),
|
|
backgroundColor: AppColors.success,
|
|
behavior: SnackBarBehavior.floating,
|
|
shape:
|
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
);
|
|
if (widget.initialDate != null) {
|
|
Navigator.pop(context);
|
|
} else {
|
|
_resetForm();
|
|
}
|
|
}
|
|
}
|
|
|
|
void _resetForm() {
|
|
setState(() {
|
|
_existingEntryId = null;
|
|
_isPeriodDay = false;
|
|
_isSpotting = false;
|
|
_flowIntensity = null;
|
|
_mood = null;
|
|
_energyLevel = 3;
|
|
_crampIntensity = 0;
|
|
_hasHeadache = false;
|
|
_hasBloating = false;
|
|
_hasBreastTenderness = false;
|
|
_hasFatigue = false;
|
|
_hasAcne = false;
|
|
_hasLowerBackPain = false;
|
|
_hasConstipation = false;
|
|
_hasDiarrhea = false;
|
|
_hasInsomnia = false;
|
|
_stressLevel = 1;
|
|
_notesController.clear();
|
|
_cravingsController.clear();
|
|
_husbandNotes = null;
|
|
_hadIntimacy = false;
|
|
_intimacyProtected = null;
|
|
_usedPantyliner = false;
|
|
_pantylinerCount = 0;
|
|
});
|
|
}
|
|
|
|
bool _shouldShowPeriodCompletionPrompt() {
|
|
final user = ref.read(userProfileProvider);
|
|
final entries = ref.read(cycleEntriesProvider);
|
|
if (user == null) return false;
|
|
|
|
// Only show for the current selected date
|
|
if (!DateUtils.isSameDay(_selectedDate, DateTime.now())) return false;
|
|
|
|
final cycleInfo = CycleService.calculateCycleInfo(user, entries);
|
|
|
|
return cycleInfo.phase == CyclePhase.menstrual && cycleInfo.dayOfCycle >= 3;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final userProfile = ref.watch(userProfileProvider);
|
|
final isPadTrackingEnabled = userProfile?.isPadTrackingEnabled ?? false;
|
|
|
|
return ProtectedContentWrapper(
|
|
title: 'Daily Log',
|
|
isProtected: userProfile?.isLogProtected ?? false,
|
|
userProfile: userProfile,
|
|
child: SafeArea(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Header
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'How are you feeling?',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 28,
|
|
fontWeight: FontWeight.w600,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
Text(
|
|
_formatDate(_selectedDate),
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
if (widget.initialDate == null)
|
|
IconButton(
|
|
onPressed: () =>
|
|
ref.read(navigationProvider.notifier).setIndex(0),
|
|
icon: const Icon(Icons.close),
|
|
style: IconButton.styleFrom(
|
|
backgroundColor: theme
|
|
.colorScheme.surfaceContainerHighest
|
|
.withValues(alpha: 0.5),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Period Toggle
|
|
_buildSectionCard(
|
|
context,
|
|
title: 'Period',
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
'Did you start your period today?',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 16,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
_buildYesNoControl(
|
|
context,
|
|
value: _isPeriodDay,
|
|
onChanged: (value) => setState(() {
|
|
_isPeriodDay = value;
|
|
if (value) _isSpotting = false;
|
|
}),
|
|
activeColor: AppColors.menstrualPhase,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Are you spotting? (only if NOT period day)
|
|
if (!_isPeriodDay) ...[
|
|
const SizedBox(height: 16),
|
|
_buildSectionCard(
|
|
context,
|
|
title: 'Spotting',
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
'Are you spotting?',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 16,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
_buildYesNoControl(
|
|
context,
|
|
value: _isSpotting,
|
|
onChanged: (value) =>
|
|
setState(() => _isSpotting = value),
|
|
activeColor: AppColors.menstrualPhase,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
|
|
// Still on Period? (If predicted but toggle is NO)
|
|
if (!_isPeriodDay && _shouldShowPeriodCompletionPrompt()) ...[
|
|
const SizedBox(height: 16),
|
|
_buildSectionCard(
|
|
context,
|
|
title: 'Period Status',
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Predicted period end is near. Is your period still going, or did it finish?',
|
|
style: GoogleFonts.outfit(fontSize: 14),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed: () =>
|
|
setState(() => _isPeriodDay = true),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: AppColors.menstrualPhase,
|
|
side: const BorderSide(
|
|
color: AppColors.menstrualPhase),
|
|
),
|
|
child: const Text('Still Going'),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: ElevatedButton(
|
|
onPressed: () {
|
|
setState(() {
|
|
_isPeriodDay = false;
|
|
_isSpotting = false;
|
|
});
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content:
|
|
Text('Period marked as finished.')),
|
|
);
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.menstrualPhase,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: const Text('Finished'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
|
|
// Flow Intensity (only if period day)
|
|
if (_isPeriodDay) ...[
|
|
const SizedBox(height: 16),
|
|
_buildSectionCard(
|
|
context,
|
|
title: 'Flow Intensity',
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: FlowIntensity.values.map((flow) {
|
|
final isSelected = _flowIntensity == flow;
|
|
return Expanded(
|
|
child: GestureDetector(
|
|
onTap: () =>
|
|
setState(() => _flowIntensity = flow),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
margin:
|
|
const EdgeInsets.symmetric(horizontal: 4),
|
|
padding:
|
|
const EdgeInsets.symmetric(vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? AppColors.menstrualPhase
|
|
.withValues(alpha: 0.2)
|
|
: theme
|
|
.colorScheme.surfaceContainerHighest,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
flow.toString().split('.').last,
|
|
style: theme.textTheme.labelLarge!.copyWith(
|
|
color: isSelected
|
|
? AppColors.menstrualPhase
|
|
: theme.colorScheme.onSurface,
|
|
fontWeight: isSelected
|
|
? FontWeight.bold
|
|
: FontWeight.normal,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
|
|
// Supply / Material Tracking
|
|
if (isPadTrackingEnabled) ...[
|
|
const SizedBox(height: 16),
|
|
_buildSectionCard(
|
|
context,
|
|
title: 'Supplies',
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: OutlinedButton.icon(
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => PadTrackerScreen(
|
|
isSpotting: _isSpotting,
|
|
initialFlow: _flowIntensity,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
icon: const Icon(Icons.timer_outlined),
|
|
label: const Text('Pad Tracker & Reminders'),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: AppColors.menstrualPhase,
|
|
side: const BorderSide(
|
|
color: AppColors.menstrualPhase),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
_getSupplyQuestionLabel(userProfile),
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 16,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
_buildYesNoControl(
|
|
context,
|
|
value: _usedPantyliner,
|
|
onChanged: (value) => setState(() {
|
|
_usedPantyliner = value;
|
|
if (!value) {
|
|
_pantylinerCount = 0;
|
|
_selectedSupplyIndex = null;
|
|
}
|
|
}),
|
|
activeColor: AppColors.menstrualPhase,
|
|
),
|
|
],
|
|
),
|
|
if (_usedPantyliner) ...[
|
|
const SizedBox(height: 12),
|
|
if (userProfile?.padSupplies?.isNotEmpty == true) ...[
|
|
Text(
|
|
'Select item from inventory:',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: List.generate(
|
|
userProfile!.padSupplies!.length, (index) {
|
|
final item = userProfile.padSupplies![index];
|
|
final isSelected = _selectedSupplyIndex == index;
|
|
return ChoiceChip(
|
|
label:
|
|
Text('${item.brand} (${item.type.label})'),
|
|
selected: isSelected,
|
|
onSelected: (selected) {
|
|
setState(() {
|
|
_selectedSupplyIndex =
|
|
selected ? index : null;
|
|
});
|
|
},
|
|
selectedColor: AppColors.menstrualPhase
|
|
.withValues(alpha: 0.2),
|
|
labelStyle: GoogleFonts.outfit(
|
|
color: isSelected
|
|
? AppColors.menstrualPhase
|
|
: theme.colorScheme.onSurface,
|
|
fontWeight: isSelected
|
|
? FontWeight.w600
|
|
: FontWeight.w400,
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
const SizedBox(height: 12),
|
|
],
|
|
TextFormField(
|
|
controller: _pantylinerCountController,
|
|
keyboardType: TextInputType.number,
|
|
decoration: InputDecoration(
|
|
labelText: 'Quantity Used',
|
|
hintText: '1',
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12)),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16, vertical: 12),
|
|
),
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_pantylinerCount = int.tryParse(value) ?? 0;
|
|
});
|
|
},
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Mood
|
|
_buildSectionCard(
|
|
context,
|
|
title: 'Mood',
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: MoodLevel.values.map((mood) {
|
|
final isSelected = _mood == mood;
|
|
return GestureDetector(
|
|
onTap: () => setState(() => _mood = mood),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? AppColors.softGold.withValues(alpha: 0.2)
|
|
: Colors.transparent,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: isSelected
|
|
? Border.all(color: AppColors.softGold)
|
|
: Border.all(color: Colors.transparent),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
mood.emoji,
|
|
style: TextStyle(
|
|
fontSize: isSelected ? 32 : 28,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
mood.label,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 10,
|
|
fontWeight: isSelected
|
|
? FontWeight.w600
|
|
: FontWeight.w400,
|
|
color: isSelected
|
|
? AppColors.softGold
|
|
: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Levels
|
|
_buildSectionCard(
|
|
context,
|
|
title: 'Daily Levels',
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 80,
|
|
child: Text(
|
|
'Energy',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Slider(
|
|
value: (_energyLevel ?? 3).toDouble(),
|
|
min: 1,
|
|
max: 5,
|
|
divisions: 4,
|
|
activeColor: AppColors.sageGreen,
|
|
onChanged: (value) {
|
|
setState(() => _energyLevel = value.round());
|
|
},
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 50,
|
|
child: Text(
|
|
_getEnergyLabel(_energyLevel),
|
|
textAlign: TextAlign.end,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 11,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 80,
|
|
child: Text(
|
|
'Stress',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Slider(
|
|
value: (_stressLevel ?? 1).toDouble(),
|
|
min: 1,
|
|
max: 5,
|
|
divisions: 4,
|
|
activeColor: AppColors.ovulationPhase,
|
|
onChanged: (value) {
|
|
setState(() => _stressLevel = value.round());
|
|
},
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 50,
|
|
child: Text(
|
|
'${_stressLevel ?? 1}/5',
|
|
textAlign: TextAlign.end,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 12,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Symptoms
|
|
_buildSectionCard(
|
|
context,
|
|
title: 'Symptoms',
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 80,
|
|
child: Text(
|
|
'Cramps',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Slider(
|
|
value: _crampIntensity.toDouble(),
|
|
min: 0,
|
|
max: 5,
|
|
divisions: 5,
|
|
activeColor: AppColors.rose,
|
|
onChanged: (value) {
|
|
setState(() => _crampIntensity = value.round());
|
|
},
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 50,
|
|
child: Text(
|
|
_crampIntensity == 0
|
|
? 'None'
|
|
: '$_crampIntensity/5',
|
|
textAlign: TextAlign.end,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 11,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: [
|
|
_buildSymptomChip(context, 'Headache', _hasHeadache,
|
|
(v) => setState(() => _hasHeadache = v)),
|
|
_buildSymptomChip(context, 'Bloating', _hasBloating,
|
|
(v) => setState(() => _hasBloating = v)),
|
|
_buildSymptomChip(
|
|
context,
|
|
'Breast Tenderness',
|
|
_hasBreastTenderness,
|
|
(v) => setState(() => _hasBreastTenderness = v)),
|
|
_buildSymptomChip(context, 'Fatigue', _hasFatigue,
|
|
(v) => setState(() => _hasFatigue = v)),
|
|
_buildSymptomChip(context, 'Acne', _hasAcne,
|
|
(v) => setState(() => _hasAcne = v)),
|
|
_buildSymptomChip(
|
|
context,
|
|
'Back Pain',
|
|
_hasLowerBackPain,
|
|
(v) => setState(() => _hasLowerBackPain = v)),
|
|
_buildSymptomChip(
|
|
context,
|
|
'Constipation',
|
|
_hasConstipation,
|
|
(v) => setState(() => _hasConstipation = v)),
|
|
_buildSymptomChip(context, 'Diarrhea', _hasDiarrhea,
|
|
(v) => setState(() => _hasDiarrhea = v)),
|
|
_buildSymptomChip(context, 'Insomnia', _hasInsomnia,
|
|
(v) => setState(() => _hasInsomnia = v)),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Cravings
|
|
_buildSectionCard(
|
|
context,
|
|
title: 'Cravings',
|
|
child: TextField(
|
|
controller: _cravingsController,
|
|
decoration: InputDecoration(
|
|
hintText: 'e.g., Chocolate, salty chips (comma separated)',
|
|
filled: true,
|
|
fillColor: theme.colorScheme.surfaceContainerHighest
|
|
.withValues(alpha: 0.1),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
),
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Intimacy
|
|
_buildSectionCard(
|
|
context,
|
|
title: 'Intimacy',
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SwitchListTile(
|
|
title: Text('Had Intimacy Today',
|
|
style: GoogleFonts.outfit(fontSize: 14)),
|
|
value: _hadIntimacy,
|
|
onChanged: (val) => setState(() {
|
|
_hadIntimacy = val;
|
|
if (!val) _intimacyProtected = null;
|
|
}),
|
|
activeThumbColor: AppColors.sageGreen,
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
if (_hadIntimacy) ...[
|
|
const SizedBox(height: 8),
|
|
Text('Protection:',
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 13, color: AppColors.warmGray)),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: GestureDetector(
|
|
onTap: () =>
|
|
setState(() => _intimacyProtected = true),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
padding:
|
|
const EdgeInsets.symmetric(vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: _intimacyProtected == true
|
|
? AppColors.sageGreen
|
|
.withValues(alpha: 0.2)
|
|
: Colors.grey.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: _intimacyProtected == true
|
|
? AppColors.sageGreen
|
|
: Colors.grey.withValues(alpha: 0.3),
|
|
),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
'Protected',
|
|
style: GoogleFonts.outfit(
|
|
fontWeight: FontWeight.w500,
|
|
color: _intimacyProtected == true
|
|
? AppColors.sageGreen
|
|
: AppColors.warmGray,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: GestureDetector(
|
|
onTap: () =>
|
|
setState(() => _intimacyProtected = false),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
padding:
|
|
const EdgeInsets.symmetric(vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: _intimacyProtected == false
|
|
? AppColors.rose.withValues(alpha: 0.15)
|
|
: Colors.grey.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: _intimacyProtected == false
|
|
? AppColors.rose
|
|
: Colors.grey.withValues(alpha: 0.3),
|
|
),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
'Unprotected',
|
|
style: GoogleFonts.outfit(
|
|
fontWeight: FontWeight.w500,
|
|
color: _intimacyProtected == false
|
|
? AppColors.rose
|
|
: AppColors.warmGray,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Notes
|
|
_buildSectionCard(
|
|
context,
|
|
title: 'Notes',
|
|
child: TextField(
|
|
controller: _notesController,
|
|
maxLines: 3,
|
|
decoration: InputDecoration(
|
|
hintText: 'Add any notes about how you\'re feeling...',
|
|
filled: true,
|
|
fillColor: theme.colorScheme.surfaceContainerHighest
|
|
.withValues(alpha: 0.1),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
),
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 14,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
// Save Button
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 54,
|
|
child: ElevatedButton(
|
|
onPressed: _saveEntry,
|
|
child: const Text('Save Entry'),
|
|
),
|
|
),
|
|
const SizedBox(height: 40),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSectionCard(BuildContext context,
|
|
{required String title, required Widget child}) {
|
|
final theme = Theme.of(context);
|
|
final isDark = theme.brightness == Brightness.dark;
|
|
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: theme.cardTheme.color,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(
|
|
color: theme.colorScheme.outline.withValues(alpha: 0.05)),
|
|
boxShadow: isDark
|
|
? null
|
|
: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: theme.colorScheme.onSurface,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
if (title == 'Supplies') Center(child: child) else child,
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildYesNoControl(BuildContext context,
|
|
{required bool value,
|
|
required ValueChanged<bool> onChanged,
|
|
required Color activeColor}) {
|
|
final theme = Theme.of(context);
|
|
final isDark = theme.brightness == Brightness.dark;
|
|
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
GestureDetector(
|
|
onTap: () => onChanged(false),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: !value
|
|
? 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.all(color: theme.colorScheme.error)
|
|
: Border.all(color: Colors.transparent),
|
|
),
|
|
child: Text(
|
|
'No',
|
|
style: GoogleFonts.outfit(
|
|
color: !value
|
|
? theme.colorScheme.error
|
|
: theme.colorScheme.onSurfaceVariant,
|
|
fontWeight: !value ? FontWeight.w600 : FontWeight.w400,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
GestureDetector(
|
|
onTap: () => onChanged(true),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: value
|
|
? 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.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,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildSymptomChip(BuildContext context, String label, bool isSelected,
|
|
ValueChanged<bool> onChanged) {
|
|
final theme = Theme.of(context);
|
|
final isDark = theme.brightness == Brightness.dark;
|
|
|
|
return Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: () => onChanged(!isSelected),
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? theme.colorScheme.tertiary
|
|
.withValues(alpha: isDark ? 0.3 : 0.2)
|
|
: theme.colorScheme.surfaceContainerHighest
|
|
.withValues(alpha: 0.3),
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: isSelected
|
|
? Border.all(color: theme.colorScheme.tertiary)
|
|
: Border.all(color: Colors.transparent),
|
|
),
|
|
child: Text(
|
|
label,
|
|
style: GoogleFonts.outfit(
|
|
fontSize: 13,
|
|
color: isSelected
|
|
? theme.colorScheme.onSurface
|
|
: theme.colorScheme.onSurfaceVariant,
|
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
String _formatDate(DateTime date) {
|
|
final now = DateTime.now();
|
|
if (DateUtils.isSameDay(date, now)) {
|
|
return 'Today, ${_getMonth(date.month)} ${date.day}';
|
|
}
|
|
const days = [
|
|
'Monday',
|
|
'Tuesday',
|
|
'Wednesday',
|
|
'Thursday',
|
|
'Friday',
|
|
'Saturday',
|
|
'Sunday'
|
|
];
|
|
return '${days[date.weekday - 1]}, ${_getMonth(date.month)} ${date.day}';
|
|
}
|
|
|
|
String _getMonth(int month) {
|
|
const months = [
|
|
'January',
|
|
'February',
|
|
'March',
|
|
'April',
|
|
'May',
|
|
'June',
|
|
'July',
|
|
'August',
|
|
'September',
|
|
'October',
|
|
'November',
|
|
'December'
|
|
];
|
|
return months[month - 1];
|
|
}
|
|
|
|
String _getEnergyLabel(int? level) {
|
|
if (level == null) return 'Not logged';
|
|
switch (level) {
|
|
case 1:
|
|
return 'Very Low';
|
|
case 2:
|
|
return 'Low';
|
|
case 3:
|
|
return 'Normal';
|
|
case 4:
|
|
return 'Good';
|
|
case 5:
|
|
return 'Excellent';
|
|
default:
|
|
return 'Normal';
|
|
}
|
|
}
|
|
|
|
String _getSupplyQuestionLabel(UserProfile? user) {
|
|
if (user == null || user.padSupplies == null || user.padSupplies!.isEmpty) {
|
|
return 'Did you use any supplies today?';
|
|
}
|
|
|
|
final hasLiners =
|
|
user.padSupplies!.any((s) => s.type == PadType.pantyLiner);
|
|
final hasPads = user.padSupplies!.any((s) => s.type != PadType.pantyLiner);
|
|
|
|
if (hasPads && hasLiners) {
|
|
return 'Did you use any supplies (pads, liners) today?';
|
|
}
|
|
if (hasLiners) return 'Did you use pantyliners today?';
|
|
return 'Did you use any supplies (pads) today?';
|
|
}
|
|
}
|