Files
Tracker/lib/screens/log/log_screen.dart

1212 lines
49 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) {
// Check if this is likely Day 1 (simplified check: no period yesterday)
// meaningful logic requires checking previous entry, but for now we trust the user logging "Is today a period day?"
// better: check if *yesterday* was NOT a period day.
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);
// If we are in menstrual phase and near the end (Day 3+)
// or if the cycle info thinks we are just past it but we haven't logged today.
return cycleInfo.phase == CyclePhase.menstrual && cycleInfo.dayOfCycle >= 3;
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
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.surfaceVariant.withOpacity(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: () {
// Keep _isPeriodDay as false, effectively marking as finished
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
.withOpacity(isDark ? 0.3 : 0.2)
: theme.colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
flow.toString().split('.').last, // Display enum name
style: theme.textTheme.labelLarge!.copyWith(
color: isSelected
? Colors.white
: theme.colorScheme.onSurface,
),
),
),
),
),
);
}).toList(),
),
],
),
),
],
// Supply / Material Tracking
if (isPadTrackingEnabled) ...[
const SizedBox(height: 16),
_buildSectionCard(
context,
title: 'Supplies',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Pad Tracker Link
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),
// Used Material Logic
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.withOpacity(0.2),
labelStyle: GoogleFonts.outfit(
color: isSelected ? AppColors.menstrualPhase : theme.colorScheme.onSurface,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
),
);
}),
),
const SizedBox(height: 12),
],
// Count Input
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
.withOpacity(isDark ? 0.3 : 0.2)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
border: isSelected
? Border.all(color: AppColors.softGold)
: Border.all(color: Colors.transparent),
),
child: Column(
children: [
Text(
mood.emoji,
style: TextStyle(
fontSize: isSelected ? 32 : 28,
),
),
const SizedBox(height: 4),
Text(
mood.label,
style: GoogleFonts.outfit(
fontSize: 10,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w400,
color: isSelected
? AppColors.softGold
: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
);
}).toList(),
),
),
const SizedBox(height: 16),
// Energy & Stress Levels
_buildSectionCard(
context,
title: 'Daily Levels',
child: Column(
children: [
// Energy Level
Row(
children: [
SizedBox(
width: 80,
child: Text(
'Energy',
style: GoogleFonts.outfit(
fontSize: 14,
color: theme.colorScheme.onSurface,
),
),
),
Expanded(
child: Slider(
value: (_energyLevel ?? 3).toDouble(),
min: 1,
max: 5,
divisions: 4,
activeColor: AppColors.sageGreen,
onChanged: (value) {
setState(() => _energyLevel = value.round());
},
),
),
SizedBox(
width: 50,
child: Text(
_getEnergyLabel(_energyLevel),
textAlign: TextAlign.end,
style: GoogleFonts.outfit(
fontSize: 11,
color: theme.colorScheme.onSurfaceVariant,
),
),
),
],
),
const SizedBox(height: 12),
// Stress Level
Row(
children: [
SizedBox(
width: 80,
child: Text(
'Stress',
style: GoogleFonts.outfit(
fontSize: 14,
color: theme.colorScheme.onSurface,
),
),
),
Expanded(
child: Slider(
value: (_stressLevel ?? 1).toDouble(),
min: 1,
max: 5,
divisions: 4,
activeColor: AppColors.ovulationPhase,
onChanged: (value) {
setState(() => _stressLevel = value.round());
},
),
),
SizedBox(
width: 50,
child: Text(
'${_stressLevel ?? 1}/5',
textAlign: TextAlign.end,
style: GoogleFonts.outfit(
fontSize: 12,
color: theme.colorScheme.onSurfaceVariant,
),
),
),
],
),
],
),
),
const SizedBox(height: 16),
// Symptoms
_buildSectionCard(
context,
title: 'Symptoms',
child: Column(
children: [
// Cramps Slider
Row(
children: [
SizedBox(
width: 80,
child: Text(
'Cramps',
style: GoogleFonts.outfit(
fontSize: 14,
color: theme.colorScheme.onSurface,
),
),
),
Expanded(
child: Slider(
value: _crampIntensity.toDouble(),
min: 0,
max: 5,
divisions: 5,
activeColor: AppColors.rose,
onChanged: (value) {
setState(() => _crampIntensity = value.round());
},
),
),
SizedBox(
width: 50,
child: Text(
_crampIntensity == 0
? 'None'
: '$_crampIntensity/5',
textAlign: TextAlign.end,
style: GoogleFonts.outfit(
fontSize: 11,
color: theme.colorScheme.onSurfaceVariant,
),
),
),
],
),
const SizedBox(height: 12),
// Symptom Toggles
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildSymptomChip(context, 'Headache', _hasHeadache,
(v) => setState(() => _hasHeadache = v)),
_buildSymptomChip(context, 'Bloating', _hasBloating,
(v) => setState(() => _hasBloating = v)),
_buildSymptomChip(context, 'Breast Tenderness',
_hasBreastTenderness,
(v) => setState(() => _hasBreastTenderness = v)),
_buildSymptomChip(context, 'Fatigue', _hasFatigue,
(v) => setState(() => _hasFatigue = v)),
_buildSymptomChip(context, 'Acne', _hasAcne,
(v) => setState(() => _hasAcne = v)),
_buildSymptomChip(context, 'Back Pain',
_hasLowerBackPain,
(v) => setState(() => _hasLowerBackPain = v)),
_buildSymptomChip(
context,
'Constipation',
_hasConstipation,
(v) => setState(() => _hasConstipation = v)),
_buildSymptomChip(context, 'Diarrhea', _hasDiarrhea,
(v) => setState(() => _hasDiarrhea = v)),
_buildSymptomChip(context, 'Insomnia', _hasInsomnia,
(v) => setState(() => _hasInsomnia = v)),
],
),
],
),
),
const SizedBox(height: 16),
// Cravings
_buildSectionCard(
context,
title: 'Cravings',
child: TextField(
controller: _cravingsController,
decoration: InputDecoration(
hintText: 'e.g., Chocolate, salty chips (comma separated)',
filled: true,
fillColor: isDark
? theme.colorScheme.surface
: theme.colorScheme.surfaceVariant.withOpacity(0.1),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
style: GoogleFonts.outfit(
fontSize: 14,
color: theme.colorScheme.onSurface,
),
),
),
const SizedBox(height: 16),
// Pantyliners
_buildSectionCard(
context,
title: 'Pantyliners',
child: Column(
children: [
SwitchListTile(
title: Text('Used Pantyliner Today', style: GoogleFonts.outfit(fontSize: 14)),
value: _usedPantyliner,
onChanged: (val) => setState(() => _usedPantyliner = val),
activeColor: AppColors.menstrualPhase,
contentPadding: EdgeInsets.zero,
),
if (_usedPantyliner) ...[
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Count', style: GoogleFonts.outfit(fontSize: 14)),
Row(
children: [
IconButton(
icon: const Icon(Icons.remove_circle_outline),
onPressed: _pantylinerCount > 0
? () => setState(() => _pantylinerCount--)
: null,
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceVariant.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'$_pantylinerCount',
style: GoogleFonts.outfit(fontWeight: FontWeight.bold),
),
),
IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () => setState(() => _pantylinerCount++),
),
],
),
],
),
],
],
),
),
const SizedBox(height: 16),
// Intimacy Tracking (for married users)
_buildSectionCard(
context,
title: 'Intimacy',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SwitchListTile(
title: Text('Had Intimacy Today', style: GoogleFonts.outfit(fontSize: 14)),
value: _hadIntimacy,
onChanged: (val) => setState(() {
_hadIntimacy = val;
if (!val) _intimacyProtected = null;
}),
activeColor: AppColors.sageGreen,
contentPadding: EdgeInsets.zero,
),
if (_hadIntimacy) ...[
const SizedBox(height: 8),
Text('Protection:', style: GoogleFonts.outfit(fontSize: 13, color: AppColors.warmGray)),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => setState(() => _intimacyProtected = true),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: _intimacyProtected == true
? AppColors.sageGreen.withOpacity(0.2)
: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _intimacyProtected == true
? AppColors.sageGreen
: Colors.grey.withOpacity(0.3),
),
),
child: Center(
child: Text(
'Protected',
style: GoogleFonts.outfit(
fontWeight: FontWeight.w500,
color: _intimacyProtected == true
? AppColors.sageGreen
: AppColors.warmGray,
),
),
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: GestureDetector(
onTap: () => setState(() => _intimacyProtected = false),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: _intimacyProtected == false
? AppColors.rose.withOpacity(0.15)
: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _intimacyProtected == false
? AppColors.rose
: Colors.grey.withOpacity(0.3),
),
),
child: Center(
child: Text(
'Unprotected',
style: GoogleFonts.outfit(
fontWeight: FontWeight.w500,
color: _intimacyProtected == false
? AppColors.rose
: AppColors.warmGray,
),
),
),
),
),
),
],
),
],
],
),
),
const SizedBox(height: 16),
// Notes
_buildSectionCard(
context,
title: 'Notes',
child: TextField(
controller: _notesController,
maxLines: 3,
decoration: InputDecoration(
hintText: 'Add any notes about how you\'re feeling...',
filled: true,
fillColor: isDark
? theme.colorScheme.surface
: theme.colorScheme.surfaceVariant.withOpacity(0.1),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
style: GoogleFonts.outfit(
fontSize: 14,
color: theme.colorScheme.onSurface,
),
),
),
const SizedBox(height: 24),
// Save Button
SizedBox(
width: double.infinity,
height: 54,
child: ElevatedButton(
onPressed: _saveEntry,
child: const Text('Save Entry'),
),
),
const SizedBox(height: 40),
],
),
)));
}
Widget _buildSectionCard(BuildContext context,
{required String title, required Widget child}) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.cardTheme.color,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.colorScheme.outline.withOpacity(0.05)),
boxShadow: isDark
? null
: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 12),
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: [
// No Button
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.withOpacity(isDark ? 0.3 : 0.2)
: theme.colorScheme.surfaceVariant.withOpacity(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,
),
),
),
),
// Yes Button
GestureDetector(
onTap: () => onChanged(true),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: value
? activeColor.withOpacity(isDark ? 0.3 : 0.2)
: theme.colorScheme.surfaceVariant.withOpacity(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.withOpacity(isDark ? 0.3 : 0.2)
: theme.colorScheme.surfaceVariant.withOpacity(0.3),
borderRadius: BorderRadius.circular(20),
border: isSelected
? Border.all(color: theme.colorScheme.tertiary)
: Border.all(color: Colors.transparent),
),
child: Text(
label,
style: GoogleFonts.outfit(
fontSize: 13,
color: isSelected
? theme.colorScheme.onSurface
: theme.colorScheme.onSurfaceVariant,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
),
),
),
),
);
}
String _formatDate(DateTime date) {
final now = DateTime.now();
if (DateUtils.isSameDay(date, now)) {
return 'Today, ${_getMonth(date.month)} ${date.day}';
}
const days = [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday'
];
return '${days[date.weekday - 1]}, ${_getMonth(date.month)} ${date.day}';
}
String _getMonth(int month) {
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
return months[month - 1];
}
String _getEnergyLabel(int? level) {
if (level == null) return 'Not logged';
switch (level) {
case 1:
return 'Very Low';
case 2:
return 'Low';
case 3:
return 'Normal';
case 4:
return 'Good';
case 5:
return 'Excellent';
default:
return 'Normal';
}
}
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);
// Assuming everything else is a "pad" or similar period protection
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?';
}
}