Enhance Pad Tracking with new Flow and Supply logic
This commit is contained in:
@@ -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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user