Files
Tracker/lib/providers/user_provider.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

249 lines
7.2 KiB
Dart

import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../models/user_profile.dart';
import '../models/cycle_entry.dart';
import '../services/cycle_service.dart';
import '../services/sync_service.dart';
import 'prayer_provider.dart';
import 'teaching_plan_provider.dart';
import '../models/prayer_request.dart';
import '../models/teaching_plan.dart';
/// Provider for the user profile
final userProfileProvider =
StateNotifierProvider<UserProfileNotifier, UserProfile?>((ref) {
return UserProfileNotifier();
});
/// Notifier for the user profile
class UserProfileNotifier extends StateNotifier<UserProfile?> {
UserProfileNotifier() : super(null) {
_loadProfile();
}
void _loadProfile() {
final box = Hive.box<UserProfile>('user_profile_v2');
state = box.get('current_user');
}
Future<void> updateProfile(UserProfile profile) async {
final box = Hive.box<UserProfile>('user_profile_v2');
await box.put('current_user', profile);
state = profile;
}
Future<void> updateThemeMode(AppThemeMode themeMode) async {
if (state != null) {
await updateProfile(state!.copyWith(themeMode: themeMode));
}
}
Future<void> updateAccentColor(String accentColor) async {
if (state != null) {
await updateProfile(state!.copyWith(accentColor: accentColor));
}
}
Future<void> updateRelationshipStatus(
RelationshipStatus relationshipStatus) async {
if (state != null) {
await updateProfile(
state!.copyWith(relationshipStatus: relationshipStatus));
}
}
Future<void> clearProfile() async {
final box = Hive.box<UserProfile>('user_profile_v2');
await box.clear();
state = null;
}
}
/// Provider for cycle entries
final cycleEntriesProvider =
StateNotifierProvider<CycleEntriesNotifier, List<CycleEntry>>((ref) {
return CycleEntriesNotifier(ref);
});
/// Notifier for cycle entries
class CycleEntriesNotifier extends StateNotifier<List<CycleEntry>> {
final Ref ref;
Timer? _syncTimer;
CycleEntriesNotifier(this.ref) : super([]) {
_loadEntries();
syncData(); // Auto-sync on load
// Start periodic sync every 10 seconds
_syncTimer = Timer.periodic(const Duration(seconds: 10), (timer) {
_pull();
});
}
@override
void dispose() {
_syncTimer?.cancel();
super.dispose();
}
void _loadEntries() {
final box = Hive.box<CycleEntry>('cycle_entries_v2');
state = box.values.toList()..sort((a, b) => b.date.compareTo(a.date));
}
Future<void> addEntry(CycleEntry entry) async {
final box = Hive.box<CycleEntry>('cycle_entries_v2');
await box.put(entry.id, entry);
_loadEntries();
_push();
}
Future<void> updateEntry(CycleEntry entry) async {
final box = Hive.box<CycleEntry>('cycle_entries_v2');
await box.put(entry.id, entry);
_loadEntries();
_push();
}
Future<void> deleteEntry(String id) async {
final box = Hive.box<CycleEntry>('cycle_entries_v2');
await box.delete(id);
_loadEntries();
_push();
}
Future<void> deleteEntriesForMonth(int year, int month) async {
final box = Hive.box<CycleEntry>('cycle_entries_v2');
final keysToDelete = <dynamic>[];
for (var entry in box.values) {
if (entry.date.year == year && entry.date.month == month) {
keysToDelete.add(entry.key);
}
}
await box.deleteAll(keysToDelete);
_loadEntries();
_push();
}
Future<void> clearEntries() async {
final box = Hive.box<CycleEntry>('cycle_entries_v2');
await box.clear();
state = [];
_push();
}
// Sync Logic
Future<void> syncData() async {
await _pull();
// After pull, we might want to push any local changes not in remote?
// For now, simpler consistency: Pull then Push current state?
// Or just Pull. Push happens on edit.
// Let's just Pull.
}
Future<void> _push() async {
final userBox = Hive.box<UserProfile>('user_profile_v2');
final user = userBox.get('current_user');
// Read current state from other providers
// Note: This relies on the providers being initialized.
final plans = ref.read(teachingPlansProvider);
final prayers = ref.read(prayerRequestsProvider);
if (user != null) {
final userDetails = {
'name': user.name,
'role': user.role.name,
'partnerId': user.partnerId,
'createdAt': user.createdAt.toIso8601String(),
// Add other relevant fields if needed for display on partner's side
};
await SyncService().pushSyncData(
userId: user.id,
entries: state,
teachingPlans: plans,
prayerRequests: prayers,
userDetails: userDetails,
);
}
}
Future<void> _pull() async {
final userBox = Hive.box<UserProfile>('user_profile_v2');
final user = userBox.get('current_user');
if (user == null) return;
final syncResult = await SyncService().pullSyncData(
user.id,
partnerId: user.partnerId,
);
// 0. Check for Server-Side Profile Updates (Auto-Link)
if (syncResult.containsKey('userProfile')) {
final serverProfile = syncResult['userProfile'] as Map<String, dynamic>;
final serverPartnerId = serverProfile['partnerId'];
// If server has a partner ID and we don't (or it's different), update!
if (serverPartnerId != null && serverPartnerId != user.partnerId) {
// Update local profile
final updatedProfile = user.copyWith(partnerId: serverPartnerId);
await Hive.box<UserProfile>('user_profile_v2')
.put('current_user', updatedProfile);
// Refresh provider state if needed, but important is to RE-SYNC with new ID
// so we get the partner's data immediately.
return _pull(); // Recursive call will now use new partnerId
}
}
final remoteEntries = syncResult['entries'] as List<CycleEntry>? ?? [];
final remotePlans =
syncResult['teachingPlans'] as List<TeachingPlan>? ?? [];
final remotePrayers =
syncResult['prayerRequests'] as List<PrayerRequest>? ?? [];
// 1. Cycle Entries
if (remoteEntries.isNotEmpty) {
final box = Hive.box<CycleEntry>('cycle_entries_v2');
// Simple merge: remote wins for id collisions
for (var entry in remoteEntries) {
await box.put(entry.id, entry);
}
_loadEntries();
}
// 2. Teaching Plans
if (remotePlans.isNotEmpty) {
final box = Hive.box<TeachingPlan>('teaching_plans_v2');
for (var plan in remotePlans) {
await box.put(plan.id, plan);
}
// Refresh provider
ref.invalidate(teachingPlansProvider);
}
// 3. Prayer Requests
if (remotePrayers.isNotEmpty) {
final box = Hive.box<PrayerRequest>('prayer_requests_v2');
for (var req in remotePrayers) {
await box.put(req.id, req);
}
// Refresh provider
ref.invalidate(prayerRequestsProvider);
}
}
// Example data generation removed
}
/// Computed provider for current cycle info
final currentCycleInfoProvider = Provider((ref) {
final user = ref.watch(userProfileProvider);
final entries = ref.watch(cycleEntriesProvider);
return CycleService.calculateCycleInfo(user, entries);
});