Refactor: Implement multi-item inventory for Pad Tracker and dynamic navigation
This commit is contained in:
@@ -6,6 +6,8 @@ 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';
|
||||
|
||||
class LogScreen extends ConsumerStatefulWidget {
|
||||
final DateTime? initialDate;
|
||||
@@ -138,6 +140,24 @@ class _LogScreenState extends ConsumerState<LogScreen> {
|
||||
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(
|
||||
@@ -261,55 +281,79 @@ class _LogScreenState extends ConsumerState<LogScreen> {
|
||||
_buildSectionCard(
|
||||
context,
|
||||
title: 'Flow Intensity',
|
||||
child: 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
|
||||
.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: isSelected
|
||||
? Border.all(color: AppColors.menstrualPhase)
|
||||
: Border.all(color: Colors.transparent),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.water_drop,
|
||||
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
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
size: 20,
|
||||
.withOpacity(isDark ? 0.3 : 0.2)
|
||||
: theme.colorScheme.surfaceVariant
|
||||
.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: isSelected
|
||||
? Border.all(color: AppColors.menstrualPhase)
|
||||
: Border.all(color: Colors.transparent),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
flow.label,
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 11,
|
||||
fontWeight: isSelected
|
||||
? FontWeight.w600
|
||||
: FontWeight.w400,
|
||||
color: isSelected
|
||||
? AppColors.menstrualPhase
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.water_drop,
|
||||
color: isSelected
|
||||
? AppColors.menstrualPhase
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
flow.label,
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 11,
|
||||
fontWeight: isSelected
|
||||
? FontWeight.w600
|
||||
: FontWeight.w400,
|
||||
color: isSelected
|
||||
? AppColors.menstrualPhase
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const PadTrackerScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.timer_outlined),
|
||||
label: const Text('Pad Tracker & Reminders'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.menstrualPhase,
|
||||
side: const BorderSide(color: AppColors.menstrualPhase),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
690
lib/screens/log/pad_tracker_screen.dart
Normal file
690
lib/screens/log/pad_tracker_screen.dart
Normal file
@@ -0,0 +1,690 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../../models/cycle_entry.dart';
|
||||
import '../../models/user_profile.dart';
|
||||
import '../../services/notification_service.dart';
|
||||
import '../../providers/user_provider.dart';
|
||||
|
||||
class PadTrackerScreen extends ConsumerStatefulWidget {
|
||||
const PadTrackerScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<PadTrackerScreen> createState() => _PadTrackerScreenState();
|
||||
}
|
||||
|
||||
class _PadTrackerScreenState extends ConsumerState<PadTrackerScreen> {
|
||||
FlowIntensity _selectedFlow = FlowIntensity.medium;
|
||||
bool _notificationScheduled = false;
|
||||
Timer? _timer;
|
||||
Duration _timeSinceLastChange = Duration.zero;
|
||||
int? _activeSupplyIndex;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_checkInitialPrompt();
|
||||
});
|
||||
_startTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startTimer() {
|
||||
_timer = Timer.periodic(const Duration(minutes: 1), (timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_updateTimeSinceChange();
|
||||
});
|
||||
}
|
||||
});
|
||||
_updateTimeSinceChange();
|
||||
}
|
||||
|
||||
void _updateTimeSinceChange() {
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user?.lastPadChangeTime != null) {
|
||||
_timeSinceLastChange = DateTime.now().difference(user!.lastPadChangeTime!);
|
||||
} else {
|
||||
_timeSinceLastChange = Duration.zero;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _checkInitialPrompt() async {
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user == null) return;
|
||||
|
||||
final lastChange = user.lastPadChangeTime;
|
||||
final now = DateTime.now();
|
||||
final bool changedToday = lastChange != null &&
|
||||
lastChange.year == now.year &&
|
||||
lastChange.month == now.month &&
|
||||
lastChange.day == now.day;
|
||||
|
||||
if (!changedToday) {
|
||||
await showDialog(
|
||||
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 && 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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateLastChangeTime(DateTime time) async {
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user != null) {
|
||||
final updatedProfile = user.copyWith(
|
||||
lastPadChangeTime: time,
|
||||
);
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
|
||||
_updateTimeSinceChange();
|
||||
}
|
||||
}
|
||||
|
||||
SupplyItem? get _activeSupply {
|
||||
final user = ref.watch(userProfileProvider);
|
||||
if (user == null || user.padSupplies == null || user.padSupplies!.isEmpty) return null;
|
||||
if (_activeSupplyIndex == null || _activeSupplyIndex! >= user.padSupplies!.length) {
|
||||
return user.padSupplies!.first;
|
||||
}
|
||||
return user.padSupplies![_activeSupplyIndex!];
|
||||
}
|
||||
|
||||
bool get _shouldShowMismatchWarning {
|
||||
final supply = _activeSupply;
|
||||
if (supply == null) return false;
|
||||
|
||||
int flowValue = 1;
|
||||
switch (_selectedFlow) {
|
||||
case FlowIntensity.spotting: flowValue = 1; break;
|
||||
case FlowIntensity.light: flowValue = 2; break;
|
||||
case FlowIntensity.medium: flowValue = 3; break;
|
||||
case FlowIntensity.heavy: flowValue = 5; break;
|
||||
}
|
||||
|
||||
return flowValue > supply.absorbency;
|
||||
}
|
||||
|
||||
int get _recommendedHours {
|
||||
final supply = _activeSupply;
|
||||
if (supply == null) return 6; // Default
|
||||
|
||||
final type = supply.type;
|
||||
|
||||
if (type == PadType.menstrualCup ||
|
||||
type == PadType.menstrualDisc ||
|
||||
type == PadType.periodUnderwear) {
|
||||
return 12;
|
||||
}
|
||||
|
||||
int baseHours;
|
||||
switch (_selectedFlow) {
|
||||
case FlowIntensity.heavy:
|
||||
baseHours = (type == PadType.super_pad || type == PadType.overnight || type == PadType.tampon_super)
|
||||
? 4
|
||||
: 3;
|
||||
break;
|
||||
case FlowIntensity.medium:
|
||||
baseHours = 6;
|
||||
break;
|
||||
case FlowIntensity.light:
|
||||
baseHours = 8;
|
||||
break;
|
||||
case FlowIntensity.spotting:
|
||||
baseHours = 8;
|
||||
break;
|
||||
}
|
||||
|
||||
int flowValue = 1;
|
||||
switch (_selectedFlow) {
|
||||
case FlowIntensity.spotting: flowValue = 1; break;
|
||||
case FlowIntensity.light: flowValue = 2; break;
|
||||
case FlowIntensity.medium: flowValue = 3; break;
|
||||
case FlowIntensity.heavy: flowValue = 5; break;
|
||||
}
|
||||
|
||||
final absorbency = supply.absorbency;
|
||||
final ratio = absorbency / flowValue;
|
||||
|
||||
double adjusted = baseHours * ratio;
|
||||
|
||||
int maxHours = (type == PadType.tampon_regular || type == PadType.tampon_super)
|
||||
? 8
|
||||
: 12;
|
||||
|
||||
if (adjusted < 1) adjusted = 1;
|
||||
if (adjusted > maxHours) adjusted = maxHours.toDouble();
|
||||
|
||||
return adjusted.round();
|
||||
}
|
||||
|
||||
void _showSupplyPicker() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => const _SupplyManagementPopup(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final remainingHours = _recommendedHours - _timeSinceLastChange.inHours;
|
||||
final isOverdue = remainingHours < 0;
|
||||
final supply = _activeSupply;
|
||||
final user = ref.watch(userProfileProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Pad Tracker'),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Supply Selection at the top as requested
|
||||
_buildSectionHeader('Current Protection'),
|
||||
const SizedBox(height: 12),
|
||||
GestureDetector(
|
||||
onTap: _showSupplyPicker,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardTheme.color,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.menstrualPhase.withOpacity(0.3)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.menstrualPhase.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.inventory_2_outlined, color: AppColors.menstrualPhase),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
supply != null ? '${supply.brand} ${supply.type.label}' : 'No Supply Selected',
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
if (supply != null)
|
||||
Text(
|
||||
'Absorbency: ${supply.absorbency}/5 • Stock: ${supply.count}',
|
||||
style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
'Tap to manage your supplies',
|
||||
style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.edit_outlined, size: 20, color: AppColors.warmGray),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
_buildSectionHeader('Current Flow Intensity'),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: FlowIntensity.values.map((flow) {
|
||||
return ChoiceChip(
|
||||
label: Text(flow.label),
|
||||
selected: _selectedFlow == flow,
|
||||
onSelected: (selected) {
|
||||
if (selected) setState(() => _selectedFlow = flow);
|
||||
},
|
||||
selectedColor: AppColors.menstrualPhase.withOpacity(0.3),
|
||||
labelStyle: GoogleFonts.outfit(
|
||||
color: _selectedFlow == flow ? AppColors.navyBlue : AppColors.charcoal,
|
||||
fontWeight: _selectedFlow == flow ? FontWeight.w600 : FontWeight.w400,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
|
||||
// Recommendation Card / Timer
|
||||
Center(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: isOverdue ? AppColors.rose.withOpacity(0.15) : AppColors.sageGreen.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: isOverdue ? AppColors.rose.withOpacity(0.3) : AppColors.sageGreen.withOpacity(0.3)
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
isOverdue ? Icons.warning_amber_rounded : Icons.timer_outlined,
|
||||
size: 48,
|
||||
color: isOverdue ? AppColors.rose : AppColors.sageGreen
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
isOverdue ? 'Change Overdue!' : 'Next Change In:',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 16,
|
||||
color: AppColors.warmGray,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (_timeSinceLastChange != Duration.zero) ...[
|
||||
Text(
|
||||
isOverdue
|
||||
? '${(-remainingHours).toString()}h overdue'
|
||||
: '${remainingHours}h ${((_recommendedHours * 60) - _timeSinceLastChange.inMinutes) % 60}m',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isOverdue ? AppColors.rose : AppColors.navyBlue,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Last changed: ${_formatDuration(_timeSinceLastChange)} ago',
|
||||
style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
|
||||
),
|
||||
] else ...[
|
||||
Text(
|
||||
'~$_recommendedHours Hours',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.navyBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
if (_shouldShowMismatchWarning)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 24),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.rose.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.rose.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.warning_amber_rounded, color: AppColors.rose),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Your flow is heavier than your protection capacity. Change sooner to avoid leaks!',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
color: AppColors.charcoal,
|
||||
fontWeight: FontWeight.w500
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: supply == null ? null : () async {
|
||||
final hours = _recommendedHours;
|
||||
|
||||
// 1. Auto-deduct inventory
|
||||
if (user != null &&
|
||||
user.isAutoInventoryEnabled) {
|
||||
|
||||
// Deduct from the active supply
|
||||
final List<SupplyItem> updatedSupplies = user.padSupplies!.map((s) {
|
||||
if (s == supply && s.count > 0) {
|
||||
return s.copyWith(count: s.count - 1);
|
||||
}
|
||||
return s;
|
||||
}).toList();
|
||||
|
||||
final updatedProfile = user.copyWith(
|
||||
padSupplies: updatedSupplies,
|
||||
lastInventoryUpdate: DateTime.now(),
|
||||
lastPadChangeTime: DateTime.now(),
|
||||
);
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
|
||||
} else if (user != null) {
|
||||
final updatedProfile = user.copyWith(
|
||||
lastPadChangeTime: DateTime.now(),
|
||||
);
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
|
||||
}
|
||||
|
||||
await NotificationService().scheduleNotification(
|
||||
id: 100,
|
||||
title: 'Time to change!',
|
||||
body: 'It\'s been $hours hours since you logged your protection.',
|
||||
scheduledDate: DateTime.now().add(Duration(hours: hours)),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_notificationScheduled = true;
|
||||
_updateTimeSinceChange();
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Logged! Timer reset & Inventory updated.')),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: Icon(_notificationScheduled ? Icons.check : Icons.restart_alt),
|
||||
label: Text(
|
||||
'Changed / Remind Me',
|
||||
style: GoogleFonts.outfit(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.menstrualPhase,
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: AppColors.warmGray.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDuration(Duration d) {
|
||||
if (d.inHours > 0) return '${d.inHours}h ${d.inMinutes % 60}m';
|
||||
return '${d.inMinutes}m';
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(String title) {
|
||||
return Text(
|
||||
title,
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.navyBlue,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SupplyManagementPopup extends ConsumerStatefulWidget {
|
||||
const _SupplyManagementPopup();
|
||||
|
||||
@override
|
||||
ConsumerState<_SupplyManagementPopup> createState() => _SupplyManagementPopupState();
|
||||
}
|
||||
|
||||
class _SupplyManagementPopupState extends ConsumerState<_SupplyManagementPopup> {
|
||||
final _brandController = TextEditingController();
|
||||
PadType _selectedType = PadType.regular;
|
||||
int _absorbency = 3;
|
||||
int _count = 20;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_brandController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _addSupply() async {
|
||||
final brand = _brandController.text.trim();
|
||||
if (brand.isEmpty) return;
|
||||
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user == null) return;
|
||||
|
||||
final newSupply = SupplyItem(
|
||||
brand: brand,
|
||||
type: _selectedType,
|
||||
absorbency: _absorbency,
|
||||
count: _count,
|
||||
);
|
||||
|
||||
final List<SupplyItem> updatedSupplies = <SupplyItem>[...(user.padSupplies ?? []), newSupply];
|
||||
final updatedProfile = user.copyWith(padSupplies: updatedSupplies);
|
||||
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
|
||||
_brandController.clear();
|
||||
setState(() {
|
||||
_count = 20;
|
||||
_absorbency = 3;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final user = ref.watch(userProfileProvider);
|
||||
final supplies = user?.padSupplies ?? [];
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
top: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Manage Supplies',
|
||||
style: GoogleFonts.outfit(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (supplies.isNotEmpty) ...[
|
||||
Text('Current Stock', style: GoogleFonts.outfit(fontWeight: FontWeight.w600, color: AppColors.navyBlue)),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
height: 120,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: supplies.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = supplies[index];
|
||||
return Container(
|
||||
width: 160,
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.warmCream.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.warmGray.withOpacity(0.2)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(item.brand, style: GoogleFonts.outfit(fontWeight: FontWeight.bold, fontSize: 13), overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final updatedSupplies = List<SupplyItem>.from(supplies)..removeAt(index);
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(user!.copyWith(padSupplies: updatedSupplies));
|
||||
},
|
||||
child: const Icon(Icons.delete_outline, size: 16, color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(item.type.label, style: GoogleFonts.outfit(fontSize: 11, color: AppColors.warmGray)),
|
||||
const Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Qty: ${item.count}', style: GoogleFonts.outfit(fontSize: 12, fontWeight: FontWeight.w600)),
|
||||
Text('Abs: ${item.absorbency}', style: GoogleFonts.outfit(fontSize: 11, color: AppColors.menstrualPhase)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
|
||||
Text('Add New Pack', style: GoogleFonts.outfit(fontWeight: FontWeight.w600, color: AppColors.navyBlue)),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _brandController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Brand Name (e.g. Always)',
|
||||
filled: true,
|
||||
fillColor: AppColors.warmCream.withOpacity(0.2),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<PadType>(
|
||||
value: _selectedType,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
items: PadType.values.map((t) => DropdownMenuItem(value: t, child: Text(t.label, style: const TextStyle(fontSize: 13)))).toList(),
|
||||
onChanged: (val) => setState(() => _selectedType = val!),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Container(
|
||||
width: 100,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(icon: const Icon(Icons.remove, size: 16), onPressed: () => setState(() => _count = (_count > 0 ? _count - 1 : 0))),
|
||||
Text('$_count', style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
IconButton(icon: const Icon(Icons.add, size: 16), onPressed: () => setState(() => _count++)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('Absorbency: $_absorbency/5', style: GoogleFonts.outfit(fontSize: 13, color: AppColors.warmGray)),
|
||||
Slider(
|
||||
value: _absorbency.toDouble(),
|
||||
min: 1, max: 5, divisions: 4,
|
||||
activeColor: AppColors.menstrualPhase,
|
||||
onChanged: (val) => setState(() => _absorbency = val.round()),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
onPressed: _addSupply,
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColors.navyBlue, foregroundColor: Colors.white),
|
||||
child: const Text('Add to Inventory'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user