Implement Notifications and Pad Tracking Enhancements

This commit is contained in:
2026-01-08 15:46:28 -06:00
parent 9ae77e7ab0
commit 512577b092
19 changed files with 3059 additions and 1576 deletions

View File

@@ -2,20 +2,23 @@ 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/user_profile.dart';
import '../../models/teaching_plan.dart';
import '../../providers/user_provider.dart';
import '../../theme/app_theme.dart';
import '../../services/bible_xml_parser.dart';
import '../../services/mock_data_service.dart';
class HusbandDevotionalScreen extends ConsumerStatefulWidget {
const HusbandDevotionalScreen({super.key});
@override
ConsumerState<HusbandDevotionalScreen> createState() => _HusbandDevotionalScreenState();
ConsumerState<HusbandDevotionalScreen> createState() =>
_HusbandDevotionalScreenState();
}
class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScreen> {
class _HusbandDevotionalScreenState
extends ConsumerState<HusbandDevotionalScreen> {
final _parser = BibleXmlParser();
Map<String, String> _scriptures = {};
bool _loading = true;
@@ -34,15 +37,16 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
Future<void> _fetchScriptures() async {
final user = ref.read(userProfileProvider);
if (user == null) return;
final translation = user.bibleTranslation;
if (translation == _currentTranslation && _scriptures.isNotEmpty) return;
setState(() => _loading = true);
try {
final assetPath = 'assets/bible_xml/${translation.name.toUpperCase()}.xml';
final assetPath =
'assets/bible_xml/${translation.name.toUpperCase()}.xml';
// Define verses to fetch
final versesToFetch = [
'1 Corinthians 11:3',
@@ -53,7 +57,7 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
];
final Map<String, String> results = {};
for (final ref in versesToFetch) {
final text = await _parser.getVerseFromAsset(assetPath, ref);
results[ref] = text ?? 'Verse not found.';
@@ -74,7 +78,8 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
void _showAddTeachingDialog([TeachingPlan? existingPlan]) {
final titleController = TextEditingController(text: existingPlan?.topic);
final scriptureController = TextEditingController(text: existingPlan?.scriptureReference);
final scriptureController =
TextEditingController(text: existingPlan?.scriptureReference);
final notesController = TextEditingController(text: existingPlan?.notes);
DateTime selectedDate = existingPlan?.date ?? DateTime.now();
@@ -125,7 +130,8 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
context: context,
initialDate: selectedDate,
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
lastDate:
DateTime.now().add(const Duration(days: 365)),
);
if (picked != null) {
setState(() => selectedDate = picked);
@@ -145,41 +151,50 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
),
ElevatedButton(
onPressed: () async {
if (titleController.text.isEmpty) return;
if (titleController.text.isEmpty) return;
final user = ref.read(userProfileProvider);
if (user == null) 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,
);
}
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);
}
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),
);
await ref.read(userProfileProvider.notifier).updateProfile(
user.copyWith(teachingPlans: updatedList),
);
if (mounted) Navigator.pop(context);
// Trigger notification for new teaching plans
if (existingPlan == null) {
NotificationService().showTeachingPlanNotification(
teacherName: user.name ?? 'Husband',
);
}
if (mounted) Navigator.pop(context);
},
child: const Text('Save'),
),
@@ -193,10 +208,11 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
final user = ref.read(userProfileProvider);
if (user == null || user.teachingPlans == null) return;
final updatedList = user.teachingPlans!.where((p) => p.id != plan.id).toList();
final updatedList =
user.teachingPlans!.where((p) => p.id != plan.id).toList();
await ref.read(userProfileProvider.notifier).updateProfile(
user.copyWith(teachingPlans: updatedList),
);
user.copyWith(teachingPlans: updatedList),
);
}
void _toggleComplete(TeachingPlan plan) async {
@@ -207,17 +223,17 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
if (p.id == plan.id) return p.copyWith(isCompleted: !p.isCompleted);
return p;
}).toList();
await ref.read(userProfileProvider.notifier).updateProfile(
user.copyWith(teachingPlans: updatedList),
);
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));
upcomingPlans.sort((a, b) => a.date.compareTo(b.date));
// Listen for translation changes to re-fetch
ref.listen(userProfileProvider, (prev, next) {
@@ -253,12 +269,13 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
),
IconButton(
onPressed: () => _showAddTeachingDialog(),
icon: const Icon(Icons.add_circle, color: AppColors.navyBlue, size: 28),
icon: const Icon(Icons.add_circle,
color: AppColors.navyBlue, size: 28),
),
],
),
const SizedBox(height: 12),
if (upcomingPlans.isEmpty)
Container(
width: double.infinity,
@@ -303,39 +320,48 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
onDismissed: (_) => _deletePlan(plan),
child: Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
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),
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,
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)),
Text(plan.scriptureReference,
style: const TextStyle(
fontWeight: FontWeight.w500)),
if (plan.notes.isNotEmpty)
Text(
plan.notes,
maxLines: 2,
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]),
),
const SizedBox(height: 4),
Text(
DateFormat.yMMMd().format(plan.date),
style: TextStyle(
fontSize: 11, color: Colors.grey[600]),
),
],
),
isThreeLine: true,
@@ -344,8 +370,13 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
);
},
),
const SizedBox(height: 40),
const SizedBox(height: 24),
// Prayer Request Section
_buildPrayerRequestSection(context, ref, user),
const SizedBox(height: 40),
],
),
),
@@ -356,11 +387,12 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
// Combine 1 Timothy verses
String timothyText = 'Loading...';
if (!_loading) {
timothyText = '${_scriptures['1 Timothy 3:4'] ?? '...'} ${_scriptures['1 Timothy 3:5'] ?? ''} ... ${_scriptures['1 Timothy 3:12'] ?? ''}';
// Cleanup potential double spaces or missing
timothyText = timothyText.replaceAll(' ', ' ').trim();
timothyText =
'${_scriptures['1 Timothy 3:4'] ?? '...'} ${_scriptures['1 Timothy 3:5'] ?? ''} ... ${_scriptures['1 Timothy 3:12'] ?? ''}';
// Cleanup potential double spaces or missing
timothyText = timothyText.replaceAll(' ', ' ').trim();
}
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
@@ -376,28 +408,31 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
const Icon(Icons.menu_book, color: Color(0xFF8B5E3C)),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Biblical Principles',
style: GoogleFonts.lora(
fontSize: 18,
fontWeight: FontWeight.bold,
color: const Color(0xFF5D4037),
),
),
Text(
version,
style: GoogleFonts.outfit(fontSize: 12, color: const Color(0xFF8B5E3C)),
),
],
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Biblical Principles',
style: GoogleFonts.lora(
fontSize: 18,
fontWeight: FontWeight.bold,
color: const Color(0xFF5D4037),
),
),
Text(
version,
style: GoogleFonts.outfit(
fontSize: 12, color: const Color(0xFF8B5E3C)),
),
],
),
],
),
const SizedBox(height: 16),
_buildVerseText(
'1 Corinthians 11:3',
_loading ? 'Loading...' : (_scriptures['1 Corinthians 11:3'] ?? 'Verse not found.'),
_loading
? 'Loading...'
: (_scriptures['1 Corinthians 11:3'] ?? 'Verse not found.'),
'Supports family structure under Christs authority.',
),
const SizedBox(height: 16),
@@ -409,9 +444,11 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
'Qualifications for church elders include managing their own households well.',
),
const SizedBox(height: 16),
_buildVerseText(
_buildVerseText(
'Titus 1:6',
_loading ? 'Loading...' : (_scriptures['Titus 1:6'] ?? 'Verse not found.'),
_loading
? 'Loading...'
: (_scriptures['Titus 1:6'] ?? 'Verse not found.'),
'Husbands who lead faithfully at home are seen as candidates for formal spiritual leadership.',
),
],
@@ -433,17 +470,17 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
),
const SizedBox(height: 4),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Text(
duration: const Duration(milliseconds: 300),
child: Text(
text,
key: ValueKey(text), // Animate change
style: GoogleFonts.lora(
fontSize: 15,
fontStyle: FontStyle.italic,
height: 1.4,
color: const Color(0xFF3E2723),
),
fontSize: 15,
fontStyle: FontStyle.italic,
height: 1.4,
color: const Color(0xFF3E2723),
),
),
),
const SizedBox(height: 4),
Text(
@@ -456,4 +493,252 @@ class _HusbandDevotionalScreenState extends ConsumerState<HusbandDevotionalScree
],
);
}
Widget _buildPrayerRequestSection(
BuildContext context, WidgetRef ref, UserProfile? user) {
// Check if connected (partnerName is set)
final isConnected =
user?.partnerName != null && (user?.partnerName?.isNotEmpty ?? false);
// Get today's cycle entry to check for prayer requests
final entries = ref.watch(cycleEntriesProvider);
final todayEntry = entries.isNotEmpty
? entries.firstWhere(
(e) => DateUtils.isSameDay(e.date, DateTime.now()),
orElse: () => entries.first,
)
: null;
final prayerRequest = todayEntry?.prayerRequest;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.lavender.withOpacity(0.15),
AppColors.blushPink.withOpacity(0.15),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.lavender.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text('🙏', style: TextStyle(fontSize: 20)),
const SizedBox(width: 8),
Expanded(
child: Text(
'Wife\'s Prayer Requests',
style: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.navyBlue,
),
),
),
],
),
const SizedBox(height: 16),
if (!isConnected) ...[
Text(
'Connect with your wife to see her prayer requests and pray for her.',
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.warmGray,
),
),
const SizedBox(height: 16),
Center(
child: ElevatedButton.icon(
onPressed: () => _showConnectDialog(context, ref),
icon: const Icon(Icons.link),
label: const Text('Connect with Wife'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.navyBlue,
foregroundColor: Colors.white,
padding:
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
),
] else if (prayerRequest != null && prayerRequest.isNotEmpty) ...[
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${user?.partnerName ?? "Wife"} shared:',
style: GoogleFonts.outfit(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.warmGray,
),
),
const SizedBox(height: 8),
Text(
prayerRequest,
style: GoogleFonts.lora(
fontSize: 15,
fontStyle: FontStyle.italic,
height: 1.5,
color: AppColors.charcoal,
),
),
],
),
),
const SizedBox(height: 12),
Center(
child: TextButton.icon(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Praying for her! 🙏'),
backgroundColor: AppColors.sageGreen,
),
);
},
icon: const Icon(Icons.favorite, size: 18),
label: const Text('I\'m Praying'),
style: TextButton.styleFrom(
foregroundColor: AppColors.rose,
),
),
),
] else ...[
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Icon(Icons.favorite_border,
color: AppColors.warmGray, size: 32),
const SizedBox(height: 8),
Text(
'No prayer requests today',
style: GoogleFonts.outfit(
fontSize: 14,
color: AppColors.warmGray,
),
),
Text(
'Check back later or encourage her to share.',
style: GoogleFonts.outfit(
fontSize: 12,
color: AppColors.warmGray.withOpacity(0.8),
),
),
],
),
),
],
],
),
);
}
void _showConnectDialog(BuildContext context, WidgetRef ref) {
final codeController = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: const [
Icon(Icons.link, color: AppColors.navyBlue),
SizedBox(width: 8),
Text('Connect with Wife'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Enter the pairing code from your wife\'s app:',
style:
GoogleFonts.outfit(fontSize: 14, color: AppColors.warmGray),
),
const SizedBox(height: 16),
TextField(
controller: codeController,
decoration: const InputDecoration(
hintText: 'e.g., ABC123',
border: OutlineInputBorder(),
),
textCapitalization: TextCapitalization.characters,
),
const SizedBox(height: 16),
Text(
'Your wife can find this code in her Devotional screen under "Share with Husband".',
style:
GoogleFonts.outfit(fontSize: 12, color: AppColors.warmGray),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () async {
final code = codeController.text.trim();
Navigator.pop(context);
if (code.isNotEmpty) {
// Simulate connection with mock data
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,
);
await ref
.read(userProfileProvider.notifier)
.updateProfile(updatedProfile);
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Connected with wife! 💑'),
backgroundColor: AppColors.sageGreen,
),
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.navyBlue,
foregroundColor: Colors.white,
),
child: const Text('Connect'),
),
],
),
);
}
}