Files
Tracker/lib/screens/husband/husband_devotional_screen.dart
Sterlen 1c2c56e9e2 feat: Add auto-sync, fix partner linking UI, update sharing settings
- Add 10-second periodic auto-sync to CycleEntriesNotifier
- Fix husband_devotional_screen: use partnerId for isConnected check, navigate to SharingSettingsScreen instead of legacy mock dialog
- Remove obsolete _showConnectDialog method and mock data import
- Update husband_settings_screen: show 'Partner Settings' with linked partner name when connected
- Add SharingSettingsScreen: Pad Supplies toggle (disabled when pad tracking off), Intimacy always enabled
- Add CORS OPTIONS handler to backend server
- Add _ensureServerRegistration for reliable partner linking
- Add copy button to Invite Partner dialog
- Dynamic base URL for web (uses window.location.hostname)
2026-01-09 17:20:49 -06:00

663 lines
23 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
import '../../services/bible_xml_parser.dart';
import '../../services/notification_service.dart';
import '../settings/sharing_settings_screen.dart';
class HusbandDevotionalScreen extends ConsumerStatefulWidget {
const HusbandDevotionalScreen({super.key});
@override
ConsumerState<HusbandDevotionalScreen> createState() =>
_HusbandDevotionalScreenState();
}
class _HusbandDevotionalScreenState
extends ConsumerState<HusbandDevotionalScreen> {
final _parser = BibleXmlParser();
Map<String, String> _scriptures = {};
bool _loading = true;
BibleTranslation? _currentTranslation;
@override
void initState() {
super.initState();
// Initial fetch handled in post-frame or via listen, but let's trigger once here if possible
// We need the ref, which is available.
WidgetsBinding.instance.addPostFrameCallback((_) {
_fetchScriptures();
});
}
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';
// Define verses to fetch
final versesToFetch = [
'1 Corinthians 11:3',
'1 Timothy 3:4',
'1 Timothy 3:5',
'1 Timothy 3:12',
'Titus 1:6',
];
final Map<String, String> results = {};
for (final ref in versesToFetch) {
final text = await _parser.getVerseFromAsset(assetPath, ref);
results[ref] = text ?? 'Verse not found.';
}
if (mounted) {
setState(() {
_scriptures = results;
_currentTranslation = translation;
_loading = false;
});
}
} catch (e) {
debugPrint('Error loading scriptures: $e');
if (mounted) setState(() => _loading = false);
}
}
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),
);
// Trigger notification for new teaching plans
if (existingPlan == null) {
NotificationService().showTeachingPlanNotification(
teacherName: user.name,
);
}
if (context.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));
// Listen for translation changes to re-fetch
ref.listen(userProfileProvider, (prev, next) {
if (next?.bibleTranslation != prev?.bibleTranslation) {
_fetchScriptures();
}
});
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(user?.bibleTranslation.label ?? 'ESV'),
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.withValues(alpha: 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.withValues(alpha: 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: 24),
// Prayer Request Section
_buildPrayerRequestSection(context, ref, user),
const SizedBox(height: 40),
],
),
),
);
}
Widget _buildHeadshipCard(String version) {
// 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();
}
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),
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)),
),
],
),
],
),
const SizedBox(height: 16),
_buildVerseText(
'1 Corinthians 11:3',
_loading
? 'Loading...'
: (_scriptures['1 Corinthians 11:3'] ?? 'Verse not found.'),
'Supports family structure under Christs authority.',
),
const SizedBox(height: 16),
const Divider(height: 1, color: Color(0xFFE0C097)),
const SizedBox(height: 16),
_buildVerseText(
'1 Timothy 3:45, 12',
timothyText,
'Qualifications for church elders include managing their own households well.',
),
const SizedBox(height: 16),
_buildVerseText(
'Titus 1:6',
_loading
? 'Loading...'
: (_scriptures['Titus 1:6'] ?? 'Verse not found.'),
'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),
AnimatedSwitcher(
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),
),
),
),
const SizedBox(height: 4),
Text(
context,
style: GoogleFonts.outfit(
fontSize: 12,
color: const Color(0xFF6D4C41),
),
),
],
);
}
Widget _buildPrayerRequestSection(
BuildContext context, WidgetRef ref, UserProfile? user) {
// Check if connected (partnerName is set)
final isConnected =
user?.partnerId != null && (user?.partnerId?.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.withValues(alpha: 0.15),
AppColors.blushPink.withValues(alpha: 0.15),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.lavender.withValues(alpha: 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: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const SharingSettingsScreen(),
),
),
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.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
const 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.withValues(alpha: 0.8),
),
),
],
),
),
],
],
),
);
}
}