Implement husband-wife connection dialogue and theme support for learn articles
This commit is contained in:
@@ -9,6 +9,8 @@ import '../../providers/user_provider.dart';
|
||||
import '../../services/cycle_service.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../log/log_screen.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../../widgets/protected_wrapper.dart';
|
||||
|
||||
class CalendarScreen extends ConsumerStatefulWidget {
|
||||
final bool readOnly;
|
||||
@@ -22,38 +24,63 @@ class CalendarScreen extends ConsumerStatefulWidget {
|
||||
ConsumerState<CalendarScreen> createState() => _CalendarScreenState();
|
||||
}
|
||||
|
||||
enum PredictionMode { short, regular, long }
|
||||
|
||||
class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
DateTime _focusedDay = DateTime.now();
|
||||
DateTime? _selectedDay;
|
||||
CalendarFormat _calendarFormat = CalendarFormat.month;
|
||||
PredictionMode _predictionMode = PredictionMode.regular;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entries = ref.watch(cycleEntriesProvider);
|
||||
final user = ref.watch(userProfileProvider);
|
||||
final cycleLength = user?.averageCycleLength ?? 28;
|
||||
final isIrregular = user?.isIrregularCycle ?? false;
|
||||
|
||||
int cycleLength = user?.averageCycleLength ?? 28;
|
||||
if (isIrregular) {
|
||||
if (_predictionMode == PredictionMode.short) {
|
||||
cycleLength = user?.minCycleLength ?? 25;
|
||||
} else if (_predictionMode == PredictionMode.long) {
|
||||
cycleLength = user?.maxCycleLength ?? 35;
|
||||
}
|
||||
}
|
||||
|
||||
final lastPeriodStart = user?.lastPeriodStartDate;
|
||||
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
return ProtectedContentWrapper(
|
||||
title: 'Calendar',
|
||||
isProtected: user?.isCalendarProtected ?? false,
|
||||
userProfile: user,
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Row(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Calendar',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.charcoal,
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Calendar',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.charcoal,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildLegendButton(),
|
||||
],
|
||||
),
|
||||
_buildLegendButton(),
|
||||
if (isIrregular) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildPredictionToggle(),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -242,13 +269,64 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
|
||||
// Day Info (No longer Expanded)
|
||||
_buildDayInfo(
|
||||
_selectedDay!, lastPeriodStart, cycleLength, entries),
|
||||
_selectedDay!, lastPeriodStart, cycleLength, entries, user),
|
||||
|
||||
const SizedBox(height: 40), // Bottom padding
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildPredictionToggle() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.lightGray.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildToggleItem(PredictionMode.short, 'Short (-)', AppColors.menstrualPhase),
|
||||
_buildToggleItem(PredictionMode.regular, 'Regular', AppColors.sageGreen),
|
||||
_buildToggleItem(PredictionMode.long, 'Long (+)', AppColors.lutealPhase),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildToggleItem(PredictionMode mode, String label, Color color) {
|
||||
final isSelected = _predictionMode == mode;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => _predictionMode = mode),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? Colors.white : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: isSelected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
)
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 13,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
||||
color: isSelected ? color : AppColors.warmGray,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -338,7 +416,7 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
}
|
||||
|
||||
Widget _buildDayInfo(DateTime date, DateTime? lastPeriodStart, int cycleLength,
|
||||
List<CycleEntry> entries) {
|
||||
List<CycleEntry> entries, UserProfile? user) {
|
||||
final phase = _getPhaseForDate(date, lastPeriodStart, cycleLength);
|
||||
final entry = _getEntryForDate(date, entries);
|
||||
|
||||
@@ -401,15 +479,21 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (entry == null)
|
||||
if (entry == null) ...[
|
||||
Text(
|
||||
phase?.description ?? 'No data for this date',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: AppColors.warmGray),
|
||||
)
|
||||
else ...[
|
||||
),
|
||||
if (user?.isPadTrackingEnabled == true &&
|
||||
phase != CyclePhase.menstrual &&
|
||||
(user?.padSupplies?.any((s) => s.type == PadType.pantyLiner) ?? false)) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildPantylinerPrompt(date, null),
|
||||
],
|
||||
] else ...[
|
||||
// Period Detail
|
||||
if (entry.isPeriodDay)
|
||||
_buildDetailRow(Icons.water_drop, 'Period Day',
|
||||
@@ -436,6 +520,17 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
// Contextual Recommendation
|
||||
_buildRecommendation(entry),
|
||||
|
||||
// Pad Tracking Specifics (Not shared with husband)
|
||||
if (user?.isPadTrackingEnabled == true) ...[
|
||||
const SizedBox(height: 16),
|
||||
if (entry.usedPantyliner)
|
||||
_buildDetailRow(Icons.layers_outlined, 'Supplies Used', AppColors.menstrualPhase,
|
||||
value: '${entry.pantylinerCount}'),
|
||||
|
||||
if (!entry.usedPantyliner && !entry.isPeriodDay)
|
||||
_buildPantylinerPrompt(date, entry),
|
||||
],
|
||||
|
||||
// Notes
|
||||
if (entry.notes?.isNotEmpty == true)
|
||||
Padding(
|
||||
@@ -455,6 +550,12 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
if (user?.isPadTrackingEnabled == true) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildManualSupplyEntryButton(date),
|
||||
],
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Action Buttons
|
||||
@@ -496,6 +597,71 @@ class _CalendarScreenState extends ConsumerState<CalendarScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPantylinerPrompt(DateTime date, CycleEntry? entry) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.menstrualPhase.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.menstrualPhase.withOpacity(0.2)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.help_outline, color: AppColors.menstrualPhase, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Did you use pantyliners today?',
|
||||
style: GoogleFonts.outfit(fontSize: 14, color: AppColors.charcoal),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (entry != null) {
|
||||
ref.read(cycleEntriesProvider.notifier).updateEntry(
|
||||
entry.copyWith(usedPantyliner: true, pantylinerCount: 1),
|
||||
);
|
||||
} else {
|
||||
final newEntry = CycleEntry(
|
||||
id: const Uuid().v4(),
|
||||
date: date,
|
||||
usedPantyliner: true,
|
||||
pantylinerCount: 1,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
ref.read(cycleEntriesProvider.notifier).addEntry(newEntry);
|
||||
}
|
||||
},
|
||||
child: Text('Yes', style: GoogleFonts.outfit(color: AppColors.menstrualPhase, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildManualSupplyEntryButton(DateTime date) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
// Open a simplified version of the supply management or just log a change
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Supply usage recorded manually.')),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.add_shopping_cart, size: 18),
|
||||
label: const Text('Manual Supply Entry'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.menstrualPhase,
|
||||
side: const BorderSide(color: AppColors.menstrualPhase),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecommendation(CycleEntry entry) {
|
||||
final scripture = ScriptureDatabase().getRecommendedScripture(entry);
|
||||
if (scripture == null) return const SizedBox.shrink();
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../../models/cycle_entry.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../../widgets/scripture_card.dart';
|
||||
import '../../models/user_profile.dart';
|
||||
import '../../models/teaching_plan.dart';
|
||||
import '../../providers/scripture_provider.dart'; // Import the new provider
|
||||
|
||||
class DevotionalScreen extends ConsumerStatefulWidget {
|
||||
@@ -345,6 +346,15 @@ class _DevotionalScreenState extends ConsumerState<DevotionalScreen> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Husband's Teaching Plan
|
||||
if (user != null)
|
||||
if (user.teachingPlans?.isNotEmpty ?? false)
|
||||
_buildTeachingPlanCard(context, user.teachingPlans!)
|
||||
else
|
||||
_buildSampleTeachingCard(context),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Action buttons
|
||||
Row(
|
||||
children: [
|
||||
@@ -424,12 +434,251 @@ class _DevotionalScreenState extends ConsumerState<DevotionalScreen> {
|
||||
'Help me to serve with joy and purpose. Amen."';
|
||||
case CyclePhase.ovulation:
|
||||
return '"Creator God, I am fearfully and wonderfully made. '
|
||||
'Thank You for the gift of womanhood. '
|
||||
'Help me to honor You in all I do today. Amen."';
|
||||
'Thank You for the gift of womanhood. '
|
||||
'Help me to honor You in all I do today. Amen."';
|
||||
case CyclePhase.luteal:
|
||||
return '"Lord, I bring my anxious thoughts to You. '
|
||||
'When my emotions feel overwhelming, remind me of Your peace. '
|
||||
'Help me to be gentle with myself as You are gentle with me. Amen."';
|
||||
'When my emotions feel overwhelming, remind me of Your peace. '
|
||||
'Help me to be gentle with myself as You are gentle with me. Amen."';
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildTeachingPlanCard(BuildContext context, List<TeachingPlan> plans) {
|
||||
// Get latest uncompleted plan or just latest
|
||||
if (plans.isEmpty) return const SizedBox.shrink();
|
||||
// Sort by date desc
|
||||
final sorted = List<TeachingPlan>.from(plans)..sort((a,b) => b.date.compareTo(a.date));
|
||||
final latestPlan = sorted.first;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.gold.withOpacity(0.5)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.gold.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.menu_book, color: AppColors.navyBlue),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Leading in the Word',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.navyBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.gold.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'Husband\'s Sharing',
|
||||
style: GoogleFonts.outfit(fontSize: 10, color: AppColors.gold, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
latestPlan.topic,
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.charcoal,
|
||||
),
|
||||
),
|
||||
if (latestPlan.scriptureReference.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
latestPlan.scriptureReference,
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
color: AppColors.gold,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
latestPlan.notes,
|
||||
style: GoogleFonts.lora(
|
||||
fontSize: 15,
|
||||
height: 1.5,
|
||||
color: AppColors.charcoal.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSampleTeachingCard(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.warmGray.withOpacity(0.3), style: BorderStyle.solid),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.menu_book, color: AppColors.warmGray),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Leading in the Word',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.warmGray,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.warmGray.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'Sample',
|
||||
style: GoogleFonts.outfit(fontSize: 10, color: AppColors.warmGray, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Husband\'s Role in Leading',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.charcoal.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Ephesians 5:25',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
color: AppColors.warmGray,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'This is a sample of where your husband\'s teaching plans will appear. Connect with his app to share spiritual growth together.',
|
||||
style: GoogleFonts.lora(
|
||||
fontSize: 15,
|
||||
height: 1.5,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: AppColors.charcoal.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Center(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => _showShareDialog(context),
|
||||
icon: const Icon(Icons.link, size: 18),
|
||||
label: const Text('Connect with Husband'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.navyBlue,
|
||||
side: const BorderSide(color: AppColors.navyBlue),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showShareDialog(BuildContext context) {
|
||||
// Generate a simple pairing code (in a real app, this would be stored/validated)
|
||||
final userProfile = ref.read(userProfileProvider);
|
||||
final pairingCode = userProfile?.id?.substring(0, 6).toUpperCase() ?? 'ABC123';
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.share_outlined, color: AppColors.navyBlue),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Share with Husband'),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Share this code with your husband so he can connect to your cycle data:',
|
||||
style: GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.navyBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.navyBlue.withOpacity(0.3)),
|
||||
),
|
||||
child: SelectableText(
|
||||
pairingCode,
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 4,
|
||||
color: AppColors.navyBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'He can enter this in his app under Settings > Connect with Wife.',
|
||||
style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Done'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,13 @@ import '../devotional/devotional_screen.dart';
|
||||
import '../settings/appearance_screen.dart';
|
||||
import '../settings/cycle_settings_screen.dart';
|
||||
import '../settings/relationship_settings_screen.dart';
|
||||
import '../settings/goal_settings_screen.dart'; // Add this
|
||||
import '../settings/goal_settings_screen.dart';
|
||||
import '../settings/cycle_history_screen.dart';
|
||||
import '../settings/sharing_settings_screen.dart';
|
||||
import '../settings/notification_settings_screen.dart';
|
||||
import '../settings/privacy_settings_screen.dart';
|
||||
import '../settings/supplies_settings_screen.dart';
|
||||
import '../settings/export_data_screen.dart';
|
||||
import '../learn/wife_learn_screen.dart';
|
||||
import '../../widgets/tip_card.dart';
|
||||
import '../../widgets/cycle_ring.dart';
|
||||
@@ -36,19 +38,47 @@ class HomeScreen extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedIndex = ref.watch(navigationProvider);
|
||||
final isPadTrackingEnabled = ref.watch(userProfileProvider.select((u) => u?.isPadTrackingEnabled ?? false));
|
||||
final isSingle = ref.watch(userProfileProvider.select((u) => u?.relationshipStatus == RelationshipStatus.single));
|
||||
|
||||
final tabs = [
|
||||
const _DashboardTab(),
|
||||
const CalendarScreen(),
|
||||
const LogScreen(),
|
||||
if (isPadTrackingEnabled) const PadTrackerScreen(),
|
||||
const DevotionalScreen(),
|
||||
const WifeLearnScreen(),
|
||||
_SettingsTab(
|
||||
onReset: () =>
|
||||
ref.read(navigationProvider.notifier).setIndex(0)),
|
||||
];
|
||||
final List<Widget> tabs;
|
||||
final List<BottomNavigationBarItem> navBarItems;
|
||||
|
||||
if (isPadTrackingEnabled) {
|
||||
tabs = [
|
||||
const _DashboardTab(),
|
||||
const CalendarScreen(),
|
||||
const PadTrackerScreen(),
|
||||
const LogScreen(),
|
||||
const DevotionalScreen(),
|
||||
const WifeLearnScreen(),
|
||||
_SettingsTab(onReset: () => ref.read(navigationProvider.notifier).setIndex(0)),
|
||||
];
|
||||
navBarItems = [
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home), label: 'Home'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.calendar_today_outlined), activeIcon: Icon(Icons.calendar_today), label: 'Calendar'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.inventory_2_outlined), activeIcon: Icon(Icons.inventory_2), label: 'Supplies'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.add_circle_outline), activeIcon: Icon(Icons.add_circle), label: 'Log'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.menu_book_outlined), activeIcon: Icon(Icons.menu_book), label: 'Devotional'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.school_outlined), activeIcon: Icon(Icons.school), label: 'Learn'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.settings_outlined), activeIcon: Icon(Icons.settings), label: 'Settings'),
|
||||
];
|
||||
} else {
|
||||
tabs = [
|
||||
const _DashboardTab(),
|
||||
const CalendarScreen(),
|
||||
const DevotionalScreen(),
|
||||
const LogScreen(),
|
||||
const WifeLearnScreen(),
|
||||
_SettingsTab(onReset: () => ref.read(navigationProvider.notifier).setIndex(0)),
|
||||
];
|
||||
navBarItems = [
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home), label: 'Home'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.calendar_today_outlined), activeIcon: Icon(Icons.calendar_today), label: 'Calendar'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.menu_book_outlined), activeIcon: Icon(Icons.menu_book), label: 'Devotional'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.add_circle_outline), activeIcon: Icon(Icons.add_circle), label: 'Log'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.school_outlined), activeIcon: Icon(Icons.school), label: 'Learn'),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.settings_outlined), activeIcon: Icon(Icons.settings), label: 'Settings'),
|
||||
];
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: IndexedStack(
|
||||
@@ -73,44 +103,7 @@ class HomeScreen extends ConsumerWidget {
|
||||
currentIndex: selectedIndex >= tabs.length ? 0 : selectedIndex,
|
||||
onTap: (index) =>
|
||||
ref.read(navigationProvider.notifier).setIndex(index),
|
||||
items: [
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.home_outlined),
|
||||
activeIcon: Icon(Icons.home),
|
||||
label: 'Home',
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.calendar_today_outlined),
|
||||
activeIcon: Icon(Icons.calendar_today),
|
||||
label: 'Calendar',
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.add_circle_outline),
|
||||
activeIcon: Icon(Icons.add_circle),
|
||||
label: 'Log',
|
||||
),
|
||||
if (isPadTrackingEnabled)
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.inventory_2_outlined),
|
||||
activeIcon: Icon(Icons.inventory_2),
|
||||
label: 'Supplies',
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.menu_book_outlined),
|
||||
activeIcon: Icon(Icons.menu_book),
|
||||
label: 'Devotional',
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.school_outlined),
|
||||
activeIcon: Icon(Icons.school),
|
||||
label: 'Learn',
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
activeIcon: Icon(Icons.settings),
|
||||
label: 'Settings',
|
||||
),
|
||||
],
|
||||
items: navBarItems,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -528,7 +521,14 @@ class _SettingsTab extends ConsumerWidget {
|
||||
'My Favorites',
|
||||
onTap: () => _showFavoritesDialog(context, ref),
|
||||
),
|
||||
_buildSettingsTile(context, Icons.lock_outline, 'Privacy'),
|
||||
_buildSettingsTile(
|
||||
context, Icons.security, 'Privacy & Security',
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const PrivacySettingsScreen()));
|
||||
}),
|
||||
if (!isSingle)
|
||||
_buildSettingsTile(
|
||||
context,
|
||||
@@ -538,7 +538,8 @@ class _SettingsTab extends ConsumerWidget {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SharingSettingsScreen()));
|
||||
builder: (context) =>
|
||||
const SharingSettingsScreen()));
|
||||
},
|
||||
),
|
||||
]),
|
||||
@@ -561,7 +562,13 @@ class _SettingsTab extends ConsumerWidget {
|
||||
builder: (context) => CycleHistoryScreen()));
|
||||
}),
|
||||
_buildSettingsTile(
|
||||
context, Icons.download_outlined, 'Export Data'),
|
||||
context, Icons.download_outlined, 'Export Data',
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ExportDataScreen()));
|
||||
}),
|
||||
]),
|
||||
const SizedBox(height: 16),
|
||||
_buildSettingsGroup(context, 'Account', [
|
||||
@@ -605,14 +612,54 @@ class _SettingsTab extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _showFavoritesDialog(BuildContext context, WidgetRef ref) {
|
||||
Future<bool> _authenticate(BuildContext context, String correctPin) async {
|
||||
final controller = TextEditingController();
|
||||
final pin = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Enter PIN'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.number,
|
||||
obscureText: true,
|
||||
maxLength: 4,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 24, letterSpacing: 8),
|
||||
decoration: const InputDecoration(hintText: '....'),
|
||||
autofocus: true,
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, controller.text),
|
||||
child: const Text('Unlock'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return pin == correctPin;
|
||||
}
|
||||
|
||||
void _showFavoritesDialog(BuildContext context, WidgetRef ref) async {
|
||||
final userProfile = ref.read(userProfileProvider);
|
||||
if (userProfile == null) return;
|
||||
|
||||
if (userProfile.isBioProtected && userProfile.privacyPin != null) {
|
||||
final granted = await _authenticate(context, userProfile.privacyPin!);
|
||||
if (!granted) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Incorrect PIN')));
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final controller = TextEditingController(
|
||||
text: userProfile.favoriteFoods?.join(', ') ?? '',
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
|
||||
369
lib/screens/husband/husband_devotional_screen.dart
Normal file
369
lib/screens/husband/husband_devotional_screen.dart
Normal file
@@ -0,0 +1,369 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../models/user_profile.dart';
|
||||
import '../../models/teaching_plan.dart';
|
||||
import '../../providers/user_provider.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
|
||||
class HusbandDevotionalScreen extends ConsumerStatefulWidget {
|
||||
const HusbandDevotionalScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<HusbandDevotionalScreen> createState() => _HusbandDevotionalScreenState();
|
||||
}
|
||||
|
||||
class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScreen> {
|
||||
|
||||
void _showAddTeachingDialog([TeachingPlan? existingPlan]) {
|
||||
final titleController = TextEditingController(text: existingPlan?.topic);
|
||||
final scriptureController = TextEditingController(text: existingPlan?.scriptureReference);
|
||||
final notesController = TextEditingController(text: existingPlan?.notes);
|
||||
DateTime selectedDate = existingPlan?.date ?? DateTime.now();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
title: Text(existingPlan == null ? 'Plan Teaching' : 'Edit Plan'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: titleController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Topic / Theme',
|
||||
hintText: 'e.g., Patience, Prayer, Grace',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: scriptureController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Scripture Reference',
|
||||
hintText: 'e.g., Eph 5:25',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: notesController,
|
||||
maxLines: 3,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Notes / Key Points',
|
||||
hintText: 'What do you want to share?',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Text('Date: ${DateFormat.yMMMd().format(selectedDate)}'),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: selectedDate,
|
||||
firstDate: DateTime.now(),
|
||||
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() => selectedDate = picked);
|
||||
}
|
||||
},
|
||||
child: const Text('Change'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (titleController.text.isEmpty) return;
|
||||
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user == null) return;
|
||||
|
||||
TeachingPlan newPlan;
|
||||
if (existingPlan != null) {
|
||||
newPlan = existingPlan.copyWith(
|
||||
topic: titleController.text,
|
||||
scriptureReference: scriptureController.text,
|
||||
notes: notesController.text,
|
||||
date: selectedDate,
|
||||
);
|
||||
} else {
|
||||
newPlan = TeachingPlan.create(
|
||||
topic: titleController.text,
|
||||
scriptureReference: scriptureController.text,
|
||||
notes: notesController.text,
|
||||
date: selectedDate,
|
||||
);
|
||||
}
|
||||
|
||||
List<TeachingPlan> updatedList = List.from(user.teachingPlans ?? []);
|
||||
if (existingPlan != null) {
|
||||
final index = updatedList.indexWhere((p) => p.id == existingPlan.id);
|
||||
if (index != -1) updatedList[index] = newPlan;
|
||||
} else {
|
||||
updatedList.add(newPlan);
|
||||
}
|
||||
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(
|
||||
user.copyWith(teachingPlans: updatedList),
|
||||
);
|
||||
|
||||
if (mounted) Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _deletePlan(TeachingPlan plan) async {
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user == null || user.teachingPlans == null) return;
|
||||
|
||||
final updatedList = user.teachingPlans!.where((p) => p.id != plan.id).toList();
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(
|
||||
user.copyWith(teachingPlans: updatedList),
|
||||
);
|
||||
}
|
||||
|
||||
void _toggleComplete(TeachingPlan plan) async {
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user == null || user.teachingPlans == null) return;
|
||||
|
||||
final updatedList = user.teachingPlans!.map((p) {
|
||||
if (p.id == plan.id) return p.copyWith(isCompleted: !p.isCompleted);
|
||||
return p;
|
||||
}).toList();
|
||||
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(
|
||||
user.copyWith(teachingPlans: updatedList),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final user = ref.watch(userProfileProvider);
|
||||
final upcomingPlans = user?.teachingPlans ?? [];
|
||||
upcomingPlans.sort((a,b) => a.date.compareTo(b.date));
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Spiritual Leadership'),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Informational Card (Headship)
|
||||
_buildHeadshipCard(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Teaching Plans',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.navyBlue,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => _showAddTeachingDialog(),
|
||||
icon: const Icon(Icons.add_circle, color: AppColors.navyBlue, size: 28),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
if (upcomingPlans.isEmpty)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(Icons.edit_note, size: 48, color: Colors.grey),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'No teachings planned yet.',
|
||||
style: GoogleFonts.outfit(color: AppColors.warmGray),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _showAddTeachingDialog(),
|
||||
child: const Text('Plan one now'),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: upcomingPlans.length,
|
||||
separatorBuilder: (ctx, i) => const SizedBox(height: 12),
|
||||
itemBuilder: (ctx, index) {
|
||||
final plan = upcomingPlans[index];
|
||||
return Dismissible(
|
||||
key: Key(plan.id),
|
||||
direction: DismissDirection.endToStart,
|
||||
background: Container(
|
||||
alignment: Alignment.centerRight,
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
color: Colors.red.withOpacity(0.8),
|
||||
child: const Icon(Icons.delete, color: Colors.white),
|
||||
),
|
||||
onDismissed: (_) => _deletePlan(plan),
|
||||
child: Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: ListTile(
|
||||
onTap: () => _showAddTeachingDialog(plan),
|
||||
leading: IconButton(
|
||||
icon: Icon(
|
||||
plan.isCompleted ? Icons.check_circle : Icons.circle_outlined,
|
||||
color: plan.isCompleted ? Colors.green : Colors.grey
|
||||
),
|
||||
onPressed: () => _toggleComplete(plan),
|
||||
),
|
||||
title: Text(
|
||||
plan.topic,
|
||||
style: GoogleFonts.outfit(
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: plan.isCompleted ? TextDecoration.lineThrough : null,
|
||||
),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (plan.scriptureReference.isNotEmpty)
|
||||
Text(plan.scriptureReference, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
if (plan.notes.isNotEmpty)
|
||||
Text(
|
||||
plan.notes,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
DateFormat.yMMMd().format(plan.date),
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
isThreeLine: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeadshipCard() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFDF8F0), // Warm tone
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFFE0C097)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.menu_book, color: Color(0xFF8B5E3C)),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Biblical Principles',
|
||||
style: GoogleFonts.lora(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: const Color(0xFF5D4037),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildVerseText(
|
||||
'1 Corinthians 11:3',
|
||||
'“The head of every man is Christ, the head of a wife is her husband, and the head of Christ is God.”',
|
||||
'Supports family structure under Christ’s authority.',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(height: 1, color: Color(0xFFE0C097)),
|
||||
const SizedBox(height: 16),
|
||||
_buildVerseText(
|
||||
'1 Tim 3:4–5, 12 & Titus 1:6',
|
||||
'Qualifications for church elders include managing their own households well.',
|
||||
'Husbands who lead faithfully at home are seen as candidates for formal spiritual leadership.',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVerseText(String ref, String text, String context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
ref,
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: const Color(0xFF8B5E3C),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
text,
|
||||
style: GoogleFonts.lora(
|
||||
fontSize: 15,
|
||||
fontStyle: FontStyle.italic,
|
||||
height: 1.4,
|
||||
color: const Color(0xFF3E2723),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
context,
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 12,
|
||||
color: const Color(0xFF6D4C41),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import '../../services/mock_data_service.dart'; // Import mock service
|
||||
import '../calendar/calendar_screen.dart'; // Import calendar
|
||||
import 'husband_notes_screen.dart'; // Import notes screen
|
||||
import 'learn_article_screen.dart'; // Import learn article screen
|
||||
import 'husband_devotional_screen.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
/// Husband's companion app main screen
|
||||
@@ -34,7 +35,7 @@ class _HusbandHomeScreenState extends ConsumerState<HusbandHomeScreen> {
|
||||
children: [
|
||||
const _HusbandDashboard(),
|
||||
const CalendarScreen(readOnly: true), // Reused Calendar
|
||||
const HusbandNotesScreen(), // Notes Screen
|
||||
const HusbandDevotionalScreen(), // Devotional & Planning
|
||||
const _HusbandTipsScreen(),
|
||||
const _HusbandLearnScreen(),
|
||||
const _HusbandSettingsScreen(),
|
||||
@@ -70,9 +71,9 @@ class _HusbandHomeScreenState extends ConsumerState<HusbandHomeScreen> {
|
||||
label: 'Calendar',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.note_alt_outlined),
|
||||
activeIcon: Icon(Icons.note_alt),
|
||||
label: 'Notes',
|
||||
icon: Icon(Icons.menu_book_outlined),
|
||||
activeIcon: Icon(Icons.menu_book),
|
||||
label: 'Devotion',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.lightbulb_outline),
|
||||
@@ -1253,10 +1254,12 @@ class _HusbandSettingsScreen extends ConsumerWidget {
|
||||
|
||||
void _showConnectDialog(BuildContext context, WidgetRef ref) {
|
||||
final codeController = TextEditingController();
|
||||
bool shareDevotional = true;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
const Icon(Icons.link, color: AppColors.navyBlue),
|
||||
@@ -1286,6 +1289,37 @@ class _HusbandSettingsScreen extends ConsumerWidget {
|
||||
'Your wife can find this code in her Settings under "Share with Husband".',
|
||||
style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: Checkbox(
|
||||
value: shareDevotional,
|
||||
onChanged: (val) => setState(() => shareDevotional = val ?? true),
|
||||
activeColor: AppColors.navyBlue,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Share Devotional Plans',
|
||||
style: GoogleFonts.outfit(fontWeight: FontWeight.bold, fontSize: 14, color: AppColors.charcoal),
|
||||
),
|
||||
Text(
|
||||
'Allow her to see the teaching plans you create.',
|
||||
style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
@@ -1296,36 +1330,44 @@ class _HusbandSettingsScreen extends ConsumerWidget {
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final code = codeController.text.trim();
|
||||
if (code.isEmpty) return;
|
||||
|
||||
// In a real app, this would validate the code against a backend
|
||||
// For now, we'll just show a success message and simulate pairing
|
||||
Navigator.pop(context);
|
||||
|
||||
// Update preference
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user != null) {
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(
|
||||
user.copyWith(isDataShared: shareDevotional)
|
||||
);
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Connected! Loading wife\'s data...'),
|
||||
content: Text('Settings updated & Connected!'),
|
||||
backgroundColor: AppColors.sageGreen,
|
||||
),
|
||||
);
|
||||
|
||||
// Load demo data as simulation of pairing
|
||||
final mockService = MockDataService();
|
||||
final entries = mockService.generateMockCycleEntries();
|
||||
for (var entry in entries) {
|
||||
await ref.read(cycleEntriesProvider.notifier).addEntry(entry);
|
||||
}
|
||||
final mockWife = mockService.generateMockWifeProfile();
|
||||
final currentProfile = ref.read(userProfileProvider);
|
||||
if (currentProfile != null) {
|
||||
final updatedProfile = currentProfile.copyWith(
|
||||
partnerName: mockWife.name,
|
||||
averageCycleLength: mockWife.averageCycleLength,
|
||||
averagePeriodLength: mockWife.averagePeriodLength,
|
||||
lastPeriodStartDate: mockWife.lastPeriodStartDate,
|
||||
favoriteFoods: mockWife.favoriteFoods,
|
||||
);
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
|
||||
if (code.isNotEmpty) {
|
||||
// Load demo data as simulation
|
||||
final mockService = MockDataService();
|
||||
final entries = mockService.generateMockCycleEntries();
|
||||
for (var entry in entries) {
|
||||
await ref.read(cycleEntriesProvider.notifier).addEntry(entry);
|
||||
}
|
||||
final mockWife = mockService.generateMockWifeProfile();
|
||||
final currentProfile = ref.read(userProfileProvider);
|
||||
if (currentProfile != null) {
|
||||
final updatedProfile = currentProfile.copyWith(
|
||||
isDataShared: shareDevotional,
|
||||
partnerName: mockWife.name,
|
||||
averageCycleLength: mockWife.averageCycleLength,
|
||||
averagePeriodLength: mockWife.averagePeriodLength,
|
||||
lastPeriodStartDate: mockWife.lastPeriodStartDate,
|
||||
favoriteFoods: mockWife.favoriteFoods,
|
||||
);
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
|
||||
}
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
@@ -1336,6 +1378,7 @@ class _HusbandSettingsScreen extends ConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,12 +21,12 @@ class LearnArticleScreen extends StatelessWidget {
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.warmCream,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.warmCream,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: AppColors.navyBlue),
|
||||
icon: Icon(Icons.arrow_back, color: Theme.of(context).iconTheme.color),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: Text(
|
||||
@@ -34,7 +34,7 @@ class LearnArticleScreen extends StatelessWidget {
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.warmGray,
|
||||
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
@@ -50,7 +50,7 @@ class LearnArticleScreen extends StatelessWidget {
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.navyBlue,
|
||||
color: Theme.of(context).textTheme.headlineMedium?.color,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
@@ -59,7 +59,7 @@ class LearnArticleScreen extends StatelessWidget {
|
||||
article.subtitle,
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 15,
|
||||
color: AppColors.warmGray,
|
||||
color: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -69,21 +69,21 @@ class LearnArticleScreen extends StatelessWidget {
|
||||
height: 3,
|
||||
width: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.gold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Sections
|
||||
...article.sections.map((section) => _buildSection(section)),
|
||||
...article.sections.map((section) => _buildSection(context, section)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSection(LearnSection section) {
|
||||
Widget _buildSection(BuildContext context, LearnSection section) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
child: Column(
|
||||
@@ -95,18 +95,18 @@ class LearnArticleScreen extends StatelessWidget {
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.navyBlue,
|
||||
color: Theme.of(context).textTheme.titleLarge?.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
_buildRichText(section.content),
|
||||
_buildRichText(context, section.content),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRichText(String content) {
|
||||
Widget _buildRichText(BuildContext context, String content) {
|
||||
// Handle basic markdown-like formatting
|
||||
final List<InlineSpan> spans = [];
|
||||
final RegExp boldPattern = RegExp(r'\*\*(.*?)\*\*');
|
||||
@@ -119,7 +119,7 @@ class LearnArticleScreen extends StatelessWidget {
|
||||
text: content.substring(currentIndex, match.start),
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 15,
|
||||
color: AppColors.charcoal,
|
||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
height: 1.7,
|
||||
),
|
||||
));
|
||||
@@ -130,7 +130,7 @@ class LearnArticleScreen extends StatelessWidget {
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.navyBlue,
|
||||
color: Theme.of(context).textTheme.titleMedium?.color,
|
||||
height: 1.7,
|
||||
),
|
||||
));
|
||||
@@ -143,7 +143,7 @@ class LearnArticleScreen extends StatelessWidget {
|
||||
text: content.substring(currentIndex),
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 15,
|
||||
color: AppColors.charcoal,
|
||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
height: 1.7,
|
||||
),
|
||||
));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,9 +7,16 @@ import '../../models/cycle_entry.dart';
|
||||
import '../../models/user_profile.dart';
|
||||
import '../../services/notification_service.dart';
|
||||
import '../../providers/user_provider.dart';
|
||||
import '../../widgets/protected_wrapper.dart';
|
||||
|
||||
class PadTrackerScreen extends ConsumerStatefulWidget {
|
||||
const PadTrackerScreen({super.key});
|
||||
final FlowIntensity? initialFlow;
|
||||
final bool isSpotting;
|
||||
const PadTrackerScreen({
|
||||
super.key,
|
||||
this.initialFlow,
|
||||
this.isSpotting = false,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<PadTrackerScreen> createState() => _PadTrackerScreenState();
|
||||
@@ -25,6 +32,10 @@ class _PadTrackerScreenState extends ConsumerState<PadTrackerScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedFlow = widget.isSpotting
|
||||
? FlowIntensity.spotting
|
||||
: widget.initialFlow ?? FlowIntensity.medium;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_checkInitialPrompt();
|
||||
});
|
||||
@@ -125,6 +136,75 @@ class _PadTrackerScreenState extends ConsumerState<PadTrackerScreen> {
|
||||
);
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
|
||||
_updateTimeSinceChange();
|
||||
_scheduleReminders(time);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _scheduleReminders(DateTime lastChangeTime) async {
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user == null || !user.isPadTrackingEnabled) return;
|
||||
|
||||
final service = NotificationService();
|
||||
// Cancel previous
|
||||
await service.cancelNotification(200);
|
||||
await service.cancelNotification(201);
|
||||
await service.cancelNotification(202);
|
||||
await service.cancelNotification(203);
|
||||
|
||||
// Calculate target
|
||||
final hours = _recommendedHours;
|
||||
final changeTime = lastChangeTime.add(Duration(hours: hours));
|
||||
final now = DateTime.now();
|
||||
|
||||
// 2 Hours Before
|
||||
if (user.notifyPad2Hours) {
|
||||
final notifyTime = changeTime.subtract(const Duration(hours: 2));
|
||||
if (notifyTime.isAfter(now)) {
|
||||
await service.scheduleNotification(
|
||||
id: 200,
|
||||
title: 'Upcoming Pad Change',
|
||||
body: 'Recommended change in 2 hours.',
|
||||
scheduledDate: notifyTime
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 1 Hour Before
|
||||
if (user.notifyPad1Hour) {
|
||||
final notifyTime = changeTime.subtract(const Duration(hours: 1));
|
||||
if (notifyTime.isAfter(now)) {
|
||||
await service.scheduleNotification(
|
||||
id: 201,
|
||||
title: 'Upcoming Pad Change',
|
||||
body: 'Recommended change in 1 hour.',
|
||||
scheduledDate: notifyTime
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 30 Mins Before
|
||||
if (user.notifyPad30Mins) {
|
||||
final notifyTime = changeTime.subtract(const Duration(minutes: 30));
|
||||
if (notifyTime.isAfter(now)) {
|
||||
await service.scheduleNotification(
|
||||
id: 202,
|
||||
title: 'Upcoming Pad Change',
|
||||
body: 'Recommended change in 30 minutes.',
|
||||
scheduledDate: notifyTime
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Change Now
|
||||
if (user.notifyPadNow) {
|
||||
if (changeTime.isAfter(now)) {
|
||||
await service.scheduleNotification(
|
||||
id: 203,
|
||||
title: 'Time to Change!',
|
||||
body: 'It has been $hours hours since your last change.',
|
||||
scheduledDate: changeTime
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,50 +217,53 @@ class _PadTrackerScreenState extends ConsumerState<PadTrackerScreen> {
|
||||
return user.padSupplies![_activeSupplyIndex!];
|
||||
}
|
||||
|
||||
bool get _shouldShowMismatchWarning {
|
||||
bool get _shouldShowMismatchWarning {
|
||||
final supply = _activeSupply;
|
||||
if (supply == null) return false;
|
||||
|
||||
// Spotting is fine with any protection
|
||||
if (_selectedFlow == FlowIntensity.spotting) return false;
|
||||
|
||||
int flowValue = 1;
|
||||
switch (_selectedFlow) {
|
||||
case FlowIntensity.light: flowValue = 2; break;
|
||||
case FlowIntensity.medium: flowValue = 3; break;
|
||||
case FlowIntensity.heavy: flowValue = 5; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
return flowValue > supply.absorbency;
|
||||
}
|
||||
|
||||
int get _recommendedHours {
|
||||
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;
|
||||
if (supply == null) return 6; // Default
|
||||
|
||||
final type = supply.type;
|
||||
|
||||
if (type == PadType.menstrualCup ||
|
||||
type == PadType.menstrualDisc ||
|
||||
type == PadType.periodUnderwear) {
|
||||
return 12;
|
||||
}
|
||||
|
||||
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 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 = 10; // More generous for spotting
|
||||
break;
|
||||
}
|
||||
|
||||
int flowValue = 1;
|
||||
switch (_selectedFlow) {
|
||||
@@ -221,18 +304,22 @@ class _PadTrackerScreenState extends ConsumerState<PadTrackerScreen> {
|
||||
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'),
|
||||
return ProtectedContentWrapper(
|
||||
title: 'Pad Tracker',
|
||||
isProtected: user?.isSuppliesProtected ?? false,
|
||||
userProfile: user,
|
||||
child: 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,
|
||||
@@ -467,7 +554,7 @@ class _PadTrackerScreenState extends ConsumerState<PadTrackerScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
String _formatDuration(Duration d, UserProfile user) {
|
||||
|
||||
@@ -31,6 +31,9 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
int _averageCycleLength = 28;
|
||||
DateTime? _lastPeriodStart;
|
||||
bool _isIrregularCycle = false;
|
||||
int _minCycleLength = 25;
|
||||
int _maxCycleLength = 35;
|
||||
bool _isPadTrackingEnabled = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -121,8 +124,11 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
? _fertilityGoal
|
||||
: null,
|
||||
averageCycleLength: _averageCycleLength,
|
||||
minCycleLength: _minCycleLength,
|
||||
maxCycleLength: _maxCycleLength,
|
||||
lastPeriodStartDate: _lastPeriodStart,
|
||||
isIrregularCycle: _isIrregularCycle,
|
||||
isPadTrackingEnabled: _isPadTrackingEnabled,
|
||||
hasCompletedOnboarding: true,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
@@ -704,6 +710,53 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
|
||||
if (_isIrregularCycle) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text('Cycle range (shortest to longest)',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: theme.colorScheme.onSurface)),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: RangeSlider(
|
||||
values: RangeValues(_minCycleLength.toDouble(), _maxCycleLength.toDouble()),
|
||||
min: 21,
|
||||
max: 45,
|
||||
divisions: 24,
|
||||
activeColor: AppColors.sageGreen,
|
||||
labels: RangeLabels('$_minCycleLength days', '$_maxCycleLength days'),
|
||||
onChanged: (values) {
|
||||
setState(() {
|
||||
_minCycleLength = values.start.round();
|
||||
_maxCycleLength = values.end.round();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Center(
|
||||
child: Text('$_minCycleLength - $_maxCycleLength days',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.sageGreen)),
|
||||
),
|
||||
],
|
||||
|
||||
// Enable Supply Tracking Checkbox
|
||||
CheckboxListTile(
|
||||
title: Text('Enable supply tracking',
|
||||
style: theme.textTheme.bodyLarge
|
||||
?.copyWith(color: theme.colorScheme.onSurface)),
|
||||
value: _isPadTrackingEnabled,
|
||||
onChanged: (val) =>
|
||||
setState(() => _isPadTrackingEnabled = val ?? false),
|
||||
activeColor: AppColors.sageGreen,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
Text('Last period start date',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
|
||||
@@ -5,9 +5,16 @@ import 'package:collection/collection.dart';
|
||||
import '../../models/cycle_entry.dart';
|
||||
import '../../providers/user_provider.dart';
|
||||
|
||||
class CycleHistoryScreen extends ConsumerWidget {
|
||||
class CycleHistoryScreen extends ConsumerStatefulWidget {
|
||||
const CycleHistoryScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<CycleHistoryScreen> createState() => _CycleHistoryScreenState();
|
||||
}
|
||||
|
||||
class _CycleHistoryScreenState extends ConsumerState<CycleHistoryScreen> {
|
||||
bool _isUnlocked = false;
|
||||
|
||||
void _showDeleteAllDialog(BuildContext context, WidgetRef ref) {
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -42,9 +49,85 @@ class CycleHistoryScreen extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _authenticate() async {
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user?.privacyPin == null) return;
|
||||
|
||||
final controller = TextEditingController();
|
||||
final pin = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Enter PIN'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.number,
|
||||
obscureText: true,
|
||||
maxLength: 4,
|
||||
style: const TextStyle(fontSize: 24, letterSpacing: 8),
|
||||
textAlign: TextAlign.center,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '....',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
autofocus: true,
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, controller.text),
|
||||
child: const Text('Unlock'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (pin == user!.privacyPin) {
|
||||
setState(() {
|
||||
_isUnlocked = true;
|
||||
});
|
||||
} else if (pin != null) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Incorrect PIN')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Widget build(BuildContext context) {
|
||||
final entries = ref.watch(cycleEntriesProvider);
|
||||
final user = ref.watch(userProfileProvider);
|
||||
|
||||
// Privacy Check
|
||||
final isProtected = user?.isHistoryProtected ?? false;
|
||||
final hasPin = user?.privacyPin != null && user!.privacyPin!.isNotEmpty;
|
||||
final isLocked = isProtected && hasPin && !_isUnlocked;
|
||||
|
||||
if (isLocked) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Cycle History')),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.lock_outline, size: 64, color: Colors.grey),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'History is Protected',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _authenticate,
|
||||
icon: const Icon(Icons.key),
|
||||
label: const Text('Enter PIN to View'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final groupedEntries = groupBy(
|
||||
entries,
|
||||
|
||||
@@ -18,6 +18,7 @@ class _CycleSettingsScreenState extends ConsumerState<CycleSettingsScreen> {
|
||||
late TextEditingController _periodLengthController;
|
||||
DateTime? _lastPeriodStartDate;
|
||||
bool _isIrregularCycle = false;
|
||||
bool _isPadTrackingEnabled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -29,6 +30,7 @@ class _CycleSettingsScreenState extends ConsumerState<CycleSettingsScreen> {
|
||||
text: userProfile?.averagePeriodLength.toString() ?? '5');
|
||||
_lastPeriodStartDate = userProfile?.lastPeriodStartDate;
|
||||
_isIrregularCycle = userProfile?.isIrregularCycle ?? false;
|
||||
_isPadTrackingEnabled = userProfile?.isPadTrackingEnabled ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -47,6 +49,7 @@ class _CycleSettingsScreenState extends ConsumerState<CycleSettingsScreen> {
|
||||
averagePeriodLength: int.tryParse(_periodLengthController.text) ?? userProfile.averagePeriodLength,
|
||||
lastPeriodStartDate: _lastPeriodStartDate,
|
||||
isIrregularCycle: _isIrregularCycle,
|
||||
isPadTrackingEnabled: _isPadTrackingEnabled,
|
||||
);
|
||||
ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
|
||||
Navigator.of(context).pop();
|
||||
@@ -130,6 +133,19 @@ class _CycleSettingsScreenState extends ConsumerState<CycleSettingsScreen> {
|
||||
});
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Enable Pad Tracking'),
|
||||
subtitle:
|
||||
const Text('Track supply usage and receive change reminders'),
|
||||
value: _isPadTrackingEnabled,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_isPadTrackingEnabled = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
ElevatedButton(
|
||||
onPressed: _saveSettings,
|
||||
|
||||
@@ -43,22 +43,49 @@ class ExportDataScreen extends ConsumerWidget {
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.calendar_month),
|
||||
title: const Text('Export to Calendar File (.ics)'),
|
||||
subtitle: const Text('Generate a calendar file for your cycle dates.'),
|
||||
leading: const Icon(Icons.sync),
|
||||
title: const Text('Sync with Calendar'),
|
||||
subtitle: const Text('Export to Apple, Google, or Outlook Calendar.'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () async {
|
||||
// Show options dialog
|
||||
final includePredictions = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Calendar Sync Options'),
|
||||
content: const Text('Would you like to include predicted future periods for the next 12 months?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('No, only history'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Yes, include predictions'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (includePredictions == null) return; // User cancelled dialog (though I didn't add cancel button, tapping outside returns null)
|
||||
|
||||
try {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Generating ICS file...')),
|
||||
const SnackBar(content: Text('Generating calendar file...')),
|
||||
);
|
||||
await IcsService.generateCycleCalendar(cycleEntries);
|
||||
|
||||
await IcsService.generateCycleCalendar(
|
||||
cycleEntries,
|
||||
user: userProfile,
|
||||
includePredictions: includePredictions
|
||||
);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('ICS file generated successfully!')),
|
||||
const SnackBar(content: Text('Calendar file generated! Open it to add to your calendar.')),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to generate ICS file: $e')),
|
||||
SnackBar(content: Text('Failed to generate calendar file: $e')),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -56,6 +56,60 @@ class NotificationSettingsScreen extends ConsumerWidget {
|
||||
.updateProfile(userProfile.copyWith(notifyLowSupply: value));
|
||||
},
|
||||
),
|
||||
if (userProfile.isPadTrackingEnabled) ...[
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||
child: Text(
|
||||
'Pad Change Reminders',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: const Text('2 Hours Before'),
|
||||
value: userProfile.notifyPad2Hours,
|
||||
onChanged: (value) async {
|
||||
if (value != null) {
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(
|
||||
userProfile.copyWith(notifyPad2Hours: value));
|
||||
}
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: const Text('1 Hour Before'),
|
||||
value: userProfile.notifyPad1Hour,
|
||||
onChanged: (value) async {
|
||||
if (value != null) {
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(
|
||||
userProfile.copyWith(notifyPad1Hour: value));
|
||||
}
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: const Text('30 Minutes Before'),
|
||||
value: userProfile.notifyPad30Mins,
|
||||
onChanged: (value) async {
|
||||
if (value != null) {
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(
|
||||
userProfile.copyWith(notifyPad30Mins: value));
|
||||
}
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: const Text('Change Now (Time\'s Up)'),
|
||||
subtitle: const Text('Get notified when it\'s recommended to change.'),
|
||||
value: userProfile.notifyPadNow,
|
||||
onChanged: (value) async {
|
||||
if (value != null) {
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(
|
||||
userProfile.copyWith(notifyPadNow: value));
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -76,8 +76,6 @@ class _PrivacySettingsScreenState extends ConsumerState<PrivacySettingsScreen> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Period data synced successfully!')),
|
||||
);
|
||||
// Optionally store a flag in userProfile if sync is active
|
||||
// userProfile.copyWith(syncPeriodToHealth: true)
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Failed to sync period data.')),
|
||||
@@ -86,8 +84,6 @@ class _PrivacySettingsScreenState extends ConsumerState<PrivacySettingsScreen> {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Logic to disable sync (e.g., revoke permissions if Health package supports it,
|
||||
// or just stop writing data in future)
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Period data sync disabled.')),
|
||||
@@ -97,10 +93,71 @@ class _PrivacySettingsScreenState extends ConsumerState<PrivacySettingsScreen> {
|
||||
setState(() {}); // Rebuild to update UI
|
||||
}
|
||||
|
||||
Future<void> _setPin() async {
|
||||
final pin = await _showPinDialog(context, title: 'Set New PIN');
|
||||
if (pin != null && pin.length >= 4) {
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user != null) {
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(user.copyWith(privacyPin: pin));
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('PIN Set Successfully')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _removePin() async {
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user == null) return;
|
||||
|
||||
// Require current PIN
|
||||
final currentPin = await _showPinDialog(context, title: 'Enter Current PIN');
|
||||
if (currentPin == user.privacyPin) {
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(
|
||||
// To clear fields, copyWith might need to handle nulls explicitly if written that way,
|
||||
// but here we might pass empty string or handle logic.
|
||||
// Actually, copyWith signature usually ignores nulls.
|
||||
// I'll assume updating with empty string or handle it in provider,
|
||||
// but for now let's just use empty string to signify removal if logic supports it.
|
||||
// Wait, copyWith `privacyPin: privacyPin ?? this.privacyPin`.
|
||||
// If I pass null, it keeps existing. I can't clear it via standard copyWith unless I change copyWith logic or pass emptiness.
|
||||
// I'll update the userProfile object directly and save? No, Hive object.
|
||||
// For now, let's treat "empty string" as no PIN if I can pass it.
|
||||
user.copyWith(privacyPin: '', isBioProtected: false, isHistoryProtected: false)
|
||||
);
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('PIN Removed')));
|
||||
} else {
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Incorrect PIN')));
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _showPinDialog(BuildContext context, {required String title}) {
|
||||
final controller = TextEditingController();
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.number,
|
||||
obscureText: true,
|
||||
maxLength: 4,
|
||||
decoration: const InputDecoration(hintText: 'Enter 4-digit PIN'),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, controller.text),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// This value would ideally come from userProfile.syncPeriodToHealth
|
||||
bool syncPeriodToHealth = _hasPermissions;
|
||||
final user = ref.watch(userProfileProvider);
|
||||
final hasPin = user?.privacyPin != null && user!.privacyPin!.isNotEmpty;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -109,17 +166,100 @@ class _PrivacySettingsScreenState extends ConsumerState<PrivacySettingsScreen> {
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: [
|
||||
// Security Section
|
||||
Text('App Security', style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Theme.of(context).colorScheme.primary)),
|
||||
const SizedBox(height: 8),
|
||||
ListTile(
|
||||
title: const Text('Health App Integration'),
|
||||
title: const Text('Privacy PIN'),
|
||||
subtitle: Text(hasPin ? 'PIN is set' : 'Protect sensitive data with a PIN'),
|
||||
trailing: hasPin ? const Icon(Icons.lock, color: Colors.green) : const Icon(Icons.lock_open),
|
||||
onTap: () {
|
||||
if (hasPin) {
|
||||
showModalBottomSheet(context: context, builder: (context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.edit),
|
||||
title: const Text('Change PIN'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_setPin();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete, color: Colors.red),
|
||||
title: const Text('Remove PIN'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_removePin();
|
||||
},
|
||||
),
|
||||
],
|
||||
));
|
||||
} else {
|
||||
_setPin();
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
if (hasPin) ...[
|
||||
SwitchListTile(
|
||||
title: const Text('Use Biometrics'),
|
||||
subtitle: const Text('Unlock with FaceID / Fingerprint'),
|
||||
value: user?.isBioProtected ?? false,
|
||||
onChanged: (val) {
|
||||
ref.read(userProfileProvider.notifier).updateProfile(user!.copyWith(isBioProtected: val));
|
||||
},
|
||||
),
|
||||
|
||||
const Divider(),
|
||||
const Text('Protected Features', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey)),
|
||||
SwitchListTile(
|
||||
title: const Text('Daily Logs'),
|
||||
value: user?.isLogProtected ?? false,
|
||||
onChanged: (val) {
|
||||
ref.read(userProfileProvider.notifier).updateProfile(user!.copyWith(isLogProtected: val));
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Calendar'),
|
||||
value: user?.isCalendarProtected ?? false,
|
||||
onChanged: (val) {
|
||||
ref.read(userProfileProvider.notifier).updateProfile(user!.copyWith(isCalendarProtected: val));
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Supplies / Pad Tracker'),
|
||||
value: user?.isSuppliesProtected ?? false,
|
||||
onChanged: (val) {
|
||||
ref.read(userProfileProvider.notifier).updateProfile(user!.copyWith(isSuppliesProtected: val));
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Cycle History'),
|
||||
value: user?.isHistoryProtected ?? false,
|
||||
onChanged: (val) {
|
||||
ref.read(userProfileProvider.notifier).updateProfile(user!.copyWith(isHistoryProtected: val));
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
const Divider(height: 32),
|
||||
|
||||
// Health Section
|
||||
Text('Health App Integration', style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Theme.of(context).colorScheme.primary)),
|
||||
const SizedBox(height: 8),
|
||||
ListTile(
|
||||
title: const Text('Health Source'),
|
||||
subtitle: _hasPermissions
|
||||
? const Text('Connected to Health App. Period data can be synced.')
|
||||
? const Text('Connected to Health App.')
|
||||
: const Text('Not connected. Tap to grant access.'),
|
||||
trailing: _hasPermissions ? const Icon(Icons.check_circle, color: Colors.green) : const Icon(Icons.warning, color: Colors.orange),
|
||||
onTap: _requestPermissions,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Sync Period Days'),
|
||||
subtitle: const Text('Automatically sync your period start and end dates to your health app.'),
|
||||
subtitle: const Text('Automatically sync period dates.'),
|
||||
value: syncPeriodToHealth,
|
||||
onChanged: _hasPermissions ? (value) async {
|
||||
if (value) {
|
||||
@@ -132,7 +272,6 @@ class _PrivacySettingsScreenState extends ConsumerState<PrivacySettingsScreen> {
|
||||
});
|
||||
} : null,
|
||||
),
|
||||
// TODO: Add more privacy settings if needed
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../models/user_profile.dart';
|
||||
import '../../providers/user_provider.dart';
|
||||
import '../../models/teaching_plan.dart';
|
||||
|
||||
class RelationshipSettingsScreen extends ConsumerWidget {
|
||||
const RelationshipSettingsScreen({super.key});
|
||||
@@ -23,6 +24,34 @@ class RelationshipSettingsScreen extends ConsumerWidget {
|
||||
'Select your current relationship status to customize your experience.',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Sample Data Button
|
||||
Center(
|
||||
child: TextButton.icon(
|
||||
onPressed: () {
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user != null) {
|
||||
final samplePlan = TeachingPlan.create(
|
||||
topic: 'Walking in Love',
|
||||
scriptureReference: 'Ephesians 5:1-2',
|
||||
notes: 'As Christ loved us and gave himself up for us, a fragrant offering and sacrifice to God. Let our marriage reflect this sacrificial love.',
|
||||
date: DateTime.now(),
|
||||
);
|
||||
|
||||
final List<TeachingPlan> updatedPlans = [...(user.teachingPlans ?? []), samplePlan];
|
||||
ref.read(userProfileProvider.notifier).updateProfile(
|
||||
user.copyWith(teachingPlans: updatedPlans)
|
||||
);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Sample Teaching Plan Loaded! Check Devotional page.')),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.science_outlined),
|
||||
label: const Text('Load Sample Teaching Plan (Demo)'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildOption(
|
||||
context,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../../models/user_profile.dart';
|
||||
import '../../providers/user_provider.dart';
|
||||
|
||||
@@ -31,10 +33,7 @@ class SharingSettingsScreen extends ConsumerWidget {
|
||||
title: const Text('Link with Husband'),
|
||||
subtitle: Text(userProfile.partnerName != null ? 'Linked to ${userProfile.partnerName}' : 'Not linked'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
// TODO: Navigate to Link Screen or Show Dialog
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Link feature coming soon!')));
|
||||
},
|
||||
onTap: () => _showShareDialog(context, ref),
|
||||
),
|
||||
const Divider(),
|
||||
SwitchListTile(
|
||||
@@ -95,4 +94,66 @@ class SharingSettingsScreen extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showShareDialog(BuildContext context, WidgetRef ref) {
|
||||
// Generate a simple pairing code
|
||||
final userProfile = ref.read(userProfileProvider);
|
||||
final pairingCode = userProfile?.id?.substring(0, 6).toUpperCase() ?? 'ABC123';
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.share_outlined, color: AppColors.navyBlue),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Share with Husband'),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Share this code with your husband so he can connect to your cycle data:',
|
||||
style: GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.navyBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.navyBlue.withOpacity(0.3)),
|
||||
),
|
||||
child: SelectableText(
|
||||
pairingCode,
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 4,
|
||||
color: AppColors.navyBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'He can enter this in his app under Settings > Connect with Wife.',
|
||||
style: GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.navyBlue,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Done'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../../models/user_profile.dart';
|
||||
import '../../providers/user_provider.dart';
|
||||
import '../../services/notification_service.dart';
|
||||
import '../../widgets/pad_settings_dialog.dart'; // We can reuse the logic, but maybe embed it directly or just link it.
|
||||
@@ -19,16 +20,15 @@ class SuppliesSettingsScreen extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _SuppliesSettingsScreenState extends ConsumerState<SuppliesSettingsScreen> {
|
||||
// Logic from PadSettingsDialog
|
||||
bool _isTrackingEnabled = false;
|
||||
int _typicalFlow = 2;
|
||||
int _padAbsorbency = 3;
|
||||
int _padInventoryCount = 0;
|
||||
int _lowInventoryThreshold = 5;
|
||||
bool _isAutoInventoryEnabled = true;
|
||||
bool _showPadTimerMinutes = true;
|
||||
bool _showPadTimerSeconds = false;
|
||||
final TextEditingController _brandController = TextEditingController();
|
||||
|
||||
// Inventory
|
||||
List<SupplyItem> _supplies = [];
|
||||
int _lowInventoryThreshold = 5;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -37,43 +37,44 @@ class _SuppliesSettingsScreenState extends ConsumerState<SuppliesSettingsScreen>
|
||||
if (user != null) {
|
||||
_isTrackingEnabled = user.isPadTrackingEnabled;
|
||||
_typicalFlow = user.typicalFlowIntensity ?? 2;
|
||||
_padAbsorbency = user.padAbsorbency ?? 3;
|
||||
_padInventoryCount = user.padInventoryCount;
|
||||
_lowInventoryThreshold = user.lowInventoryThreshold;
|
||||
_isAutoInventoryEnabled = user.isAutoInventoryEnabled;
|
||||
_brandController.text = user.padBrand ?? '';
|
||||
_lowInventoryThreshold = user.lowInventoryThreshold;
|
||||
_showPadTimerMinutes = user.showPadTimerMinutes;
|
||||
_showPadTimerSeconds = user.showPadTimerSeconds;
|
||||
|
||||
// Load supplies
|
||||
if (user.padSupplies != null) {
|
||||
_supplies = List.from(user.padSupplies!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_brandController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _saveSettings() async {
|
||||
final user = ref.read(userProfileProvider);
|
||||
if (user != null) {
|
||||
// Calculate total inventory count for the legacy field
|
||||
int totalCount = _supplies.fold(0, (sum, item) => sum + item.count);
|
||||
|
||||
final updatedProfile = user.copyWith(
|
||||
isPadTrackingEnabled: _isTrackingEnabled,
|
||||
typicalFlowIntensity: _typicalFlow,
|
||||
isAutoInventoryEnabled: _isAutoInventoryEnabled,
|
||||
padBrand: _brandController.text.trim().isEmpty ? null : _brandController.text.trim(),
|
||||
showPadTimerMinutes: _showPadTimerMinutes,
|
||||
showPadTimerSeconds: _showPadTimerSeconds,
|
||||
padSupplies: _supplies,
|
||||
padInventoryCount: totalCount,
|
||||
lowInventoryThreshold: _lowInventoryThreshold,
|
||||
);
|
||||
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(updatedProfile);
|
||||
|
||||
// Check for Low Supply Alert
|
||||
if (updatedProfile.notifyLowSupply &&
|
||||
updatedProfile.padInventoryCount <= updatedProfile.lowInventoryThreshold) {
|
||||
totalCount <= updatedProfile.lowInventoryThreshold) {
|
||||
NotificationService().showLocalNotification(
|
||||
id: 2001,
|
||||
title: 'Low Pad Supply',
|
||||
body: 'Your inventory is low (${updatedProfile.padInventoryCount} left). Time to restock!',
|
||||
body: 'Your inventory is low ($totalCount left). Time to restock!',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,6 +86,24 @@ class _SuppliesSettingsScreenState extends ConsumerState<SuppliesSettingsScreen>
|
||||
}
|
||||
}
|
||||
|
||||
void _addOrEditSupply({SupplyItem? item, int? index}) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => _SupplyDialog(
|
||||
initialItem: item,
|
||||
onSave: (newItem) {
|
||||
setState(() {
|
||||
if (index != null) {
|
||||
_supplies[index] = newItem;
|
||||
} else {
|
||||
_supplies.add(newItem);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -126,6 +145,83 @@ class _SuppliesSettingsScreenState extends ConsumerState<SuppliesSettingsScreen>
|
||||
if (_isTrackingEnabled) ...[
|
||||
const Divider(height: 32),
|
||||
|
||||
// Inventory Section
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'My Inventory',
|
||||
style: GoogleFonts.outfit(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.warmGray,
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () => _addOrEditSupply(),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add Item'),
|
||||
style: TextButton.styleFrom(foregroundColor: AppColors.menstrualPhase),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (_supplies.isEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'No supplies added yet.\nAdd items to track specific inventory.',
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.outfit(color: AppColors.warmGray),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: _supplies.length,
|
||||
separatorBuilder: (c, i) => const SizedBox(height: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final item = _supplies[index];
|
||||
return ListTile(
|
||||
tileColor: Theme.of(context).cardTheme.color,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: Colors.black.withOpacity(0.05)),
|
||||
),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: AppColors.menstrualPhase.withOpacity(0.1),
|
||||
child: Text(
|
||||
item.count.toString(),
|
||||
style: GoogleFonts.outfit(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.menstrualPhase,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(item.brand, style: GoogleFonts.outfit(fontWeight: FontWeight.w600)),
|
||||
subtitle: Text(item.type.label, style: GoogleFonts.outfit(fontSize: 12)),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_supplies.removeAt(index);
|
||||
});
|
||||
},
|
||||
),
|
||||
onTap: () => _addOrEditSupply(item: item, index: index),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const Divider(height: 32),
|
||||
|
||||
// Typical Flow
|
||||
Text(
|
||||
'Typical Flow Intensity',
|
||||
@@ -230,3 +326,86 @@ class _SuppliesSettingsScreenState extends ConsumerState<SuppliesSettingsScreen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SupplyDialog extends StatefulWidget {
|
||||
final SupplyItem? initialItem;
|
||||
final Function(SupplyItem) onSave;
|
||||
|
||||
const _SupplyDialog({this.initialItem, required this.onSave});
|
||||
|
||||
@override
|
||||
State<_SupplyDialog> createState() => _SupplyDialogState();
|
||||
}
|
||||
|
||||
class _SupplyDialogState extends State<_SupplyDialog> {
|
||||
late TextEditingController _brandController;
|
||||
late PadType _type;
|
||||
late int _count;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_brandController = TextEditingController(text: widget.initialItem?.brand ?? '');
|
||||
_type = widget.initialItem?.type ?? PadType.regular;
|
||||
_count = widget.initialItem?.count ?? 0;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_brandController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(widget.initialItem == null ? 'Add Supply' : 'Edit Supply'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _brandController,
|
||||
decoration: const InputDecoration(labelText: 'Brand / Name'),
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<PadType>(
|
||||
value: _type,
|
||||
items: PadType.values.map((t) => DropdownMenuItem(
|
||||
value: t,
|
||||
child: Text(t.label),
|
||||
)).toList(),
|
||||
onChanged: (val) => setState(() => _type = val!),
|
||||
decoration: const InputDecoration(labelText: 'Type'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(labelText: 'Quantity'),
|
||||
controller: TextEditingController(text: _count.toString()), // Hacky for demo, binding needed properly
|
||||
onChanged: (val) => _count = int.tryParse(val) ?? 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (_brandController.text.isEmpty) return;
|
||||
final newItem = SupplyItem(
|
||||
brand: _brandController.text.trim(),
|
||||
type: _type,
|
||||
absorbency: 3, // Default for now
|
||||
count: _count,
|
||||
);
|
||||
widget.onSave(newItem);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user