Enhance Pad Tracking with new Flow and Supply logic

This commit is contained in:
2026-01-09 13:35:07 -06:00
parent 24ffac2415
commit dc6bcad83f
17 changed files with 765 additions and 171 deletions

View File

@@ -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(

View File

@@ -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),

View File

@@ -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,
),
),
],

View File

@@ -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,
),
),

View File

@@ -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(

View File

@@ -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,
),
),
),

View File

@@ -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'),
),
],
);
}
}

View File

@@ -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(

View File

@@ -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) {