Implement data sync and cleanup
This commit is contained in:
@@ -3,8 +3,8 @@ import 'package:hive_flutter/hive_flutter.dart';
|
||||
import '../models/user_profile.dart';
|
||||
import '../models/cycle_entry.dart';
|
||||
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../services/cycle_service.dart';
|
||||
import '../services/sync_service.dart';
|
||||
|
||||
/// Provider for the user profile
|
||||
final userProfileProvider =
|
||||
@@ -66,6 +66,7 @@ final cycleEntriesProvider =
|
||||
class CycleEntriesNotifier extends StateNotifier<List<CycleEntry>> {
|
||||
CycleEntriesNotifier() : super([]) {
|
||||
_loadEntries();
|
||||
syncData(); // Auto-sync on load
|
||||
}
|
||||
|
||||
void _loadEntries() {
|
||||
@@ -77,18 +78,21 @@ class CycleEntriesNotifier extends StateNotifier<List<CycleEntry>> {
|
||||
final box = Hive.box<CycleEntry>('cycle_entries');
|
||||
await box.put(entry.id, entry);
|
||||
_loadEntries();
|
||||
_push();
|
||||
}
|
||||
|
||||
Future<void> updateEntry(CycleEntry entry) async {
|
||||
final box = Hive.box<CycleEntry>('cycle_entries');
|
||||
await box.put(entry.id, entry);
|
||||
_loadEntries();
|
||||
_push();
|
||||
}
|
||||
|
||||
Future<void> deleteEntry(String id) async {
|
||||
final box = Hive.box<CycleEntry>('cycle_entries');
|
||||
await box.delete(id);
|
||||
_loadEntries();
|
||||
_push();
|
||||
}
|
||||
|
||||
Future<void> deleteEntriesForMonth(int year, int month) async {
|
||||
@@ -101,63 +105,52 @@ class CycleEntriesNotifier extends StateNotifier<List<CycleEntry>> {
|
||||
}
|
||||
await box.deleteAll(keysToDelete);
|
||||
_loadEntries();
|
||||
_push();
|
||||
}
|
||||
|
||||
Future<void> clearEntries() async {
|
||||
final box = Hive.box<CycleEntry>('cycle_entries');
|
||||
await box.clear();
|
||||
state = [];
|
||||
_push();
|
||||
}
|
||||
|
||||
Future<void> generateExampleData(String userId) async {
|
||||
await clearEntries();
|
||||
final box = Hive.box<CycleEntry>('cycle_entries');
|
||||
final today = DateTime.now();
|
||||
// Sync Logic
|
||||
|
||||
// Generate 3 past cycles (~28 days each)
|
||||
DateTime cycleStart = today.subtract(const Duration(days: 84)); // 3 * 28
|
||||
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.
|
||||
}
|
||||
|
||||
while (cycleStart.isBefore(today)) {
|
||||
// Create Period (5 days)
|
||||
for (int i = 0; i < 5; i++) {
|
||||
final date = cycleStart.add(Duration(days: i));
|
||||
if (date.isAfter(today)) break;
|
||||
|
||||
final isHeavy = i == 1 || i == 2;
|
||||
final entry = CycleEntry(
|
||||
id: const Uuid().v4(),
|
||||
date: date,
|
||||
isPeriodDay: true,
|
||||
flowIntensity: isHeavy ? FlowIntensity.heavy : FlowIntensity.medium,
|
||||
mood: i == 1 ? MoodLevel.sad : null,
|
||||
crampIntensity: i == 0 ? 3 : null,
|
||||
hasHeadache: i == 0,
|
||||
createdAt: date,
|
||||
updatedAt: date,
|
||||
);
|
||||
await box.put(entry.id, entry);
|
||||
}
|
||||
|
||||
// Add random ovulation symptoms near day 14
|
||||
final ovulationDay = cycleStart.add(const Duration(days: 14));
|
||||
if (ovulationDay.isBefore(today)) {
|
||||
final entry = CycleEntry(
|
||||
id: const Uuid().v4(),
|
||||
date: ovulationDay,
|
||||
isPeriodDay: false,
|
||||
energyLevel: 4, // High energy
|
||||
mood: MoodLevel.veryHappy,
|
||||
createdAt: ovulationDay,
|
||||
updatedAt: ovulationDay,
|
||||
);
|
||||
await box.put(entry.id, entry);
|
||||
}
|
||||
|
||||
cycleStart = cycleStart.add(const Duration(days: 28));
|
||||
Future<void> _push() async {
|
||||
final userBox = Hive.box<UserProfile>('user_profile');
|
||||
final user = userBox.get('current_user');
|
||||
if (user != null) {
|
||||
await SyncService().pushSyncData(user.id, state);
|
||||
}
|
||||
|
||||
_loadEntries();
|
||||
}
|
||||
|
||||
Future<void> _pull() async {
|
||||
final userBox = Hive.box<UserProfile>('user_profile');
|
||||
final user = userBox.get('current_user');
|
||||
if (user == null) return;
|
||||
|
||||
final remoteEntries = await SyncService().pullSyncData(user.id);
|
||||
if (remoteEntries.isNotEmpty) {
|
||||
final box = Hive.box<CycleEntry>('cycle_entries');
|
||||
// Simple merge: Remote wins or Union?
|
||||
// Union: Upsert all.
|
||||
for (var entry in remoteEntries) {
|
||||
await box.put(entry.id, entry);
|
||||
}
|
||||
_loadEntries();
|
||||
}
|
||||
}
|
||||
|
||||
// Example data generation removed
|
||||
}
|
||||
|
||||
/// Computed provider for current cycle info
|
||||
|
||||
@@ -620,6 +620,25 @@ class _SettingsTab extends ConsumerWidget {
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ExportDataScreen()));
|
||||
}),
|
||||
_buildSettingsTile(
|
||||
context,
|
||||
Icons.sync,
|
||||
'Sync Data',
|
||||
onTap: () async {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Syncing data...')),
|
||||
);
|
||||
await ref.read(cycleEntriesProvider.notifier).syncData();
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Sync complete'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 16),
|
||||
_buildSettingsGroup(context, 'Account', [
|
||||
|
||||
@@ -390,6 +390,27 @@ class HusbandSettingsScreen extends ConsumerWidget {
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.sync, color: Colors.blue),
|
||||
title: Text('Sync Data',
|
||||
style: GoogleFonts.outfit(
|
||||
fontWeight: FontWeight.w500, color: Colors.blue)),
|
||||
onTap: () async {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Syncing data...')),
|
||||
);
|
||||
await ref.read(cycleEntriesProvider.notifier).syncData();
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Sync complete'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.cloud_download_outlined,
|
||||
color: Colors.blue),
|
||||
|
||||
@@ -141,7 +141,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
lastPeriodStartDate: _lastPeriodStart,
|
||||
isIrregularCycle: _isIrregularCycle,
|
||||
hasCompletedOnboarding: true,
|
||||
useExampleData: _useExampleData,
|
||||
// useExampleData: Removed
|
||||
isPadTrackingEnabled: _isPadTrackingEnabled,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
@@ -149,15 +149,17 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(userProfile);
|
||||
|
||||
// Generate example data if requested
|
||||
// Generate example data if requested - REMOVED
|
||||
/*
|
||||
if (_useExampleData) {
|
||||
await ref
|
||||
.read(cycleEntriesProvider.notifier)
|
||||
.generateExampleData(userProfile.id);
|
||||
}
|
||||
*/
|
||||
|
||||
// Trigger partner connection notification if applicable
|
||||
if (!_skipPartnerConnection && !_useExampleData) {
|
||||
if (!_skipPartnerConnection) {
|
||||
await NotificationService().showPartnerUpdateNotification(
|
||||
title: 'Connection Successful!',
|
||||
body: 'You are now connected with your partner. Tap to start sharing.',
|
||||
|
||||
135
lib/services/sync_service.dart
Normal file
135
lib/services/sync_service.dart
Normal file
@@ -0,0 +1,135 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../models/cycle_entry.dart';
|
||||
|
||||
class SyncService {
|
||||
static const String _baseUrl = 'http://localhost:8090';
|
||||
|
||||
// Push data to backend
|
||||
Future<void> pushSyncData(String userId, List<CycleEntry> entries) async {
|
||||
try {
|
||||
final url = Uri.parse('$_baseUrl/sync/push');
|
||||
final payload = {
|
||||
'userId': userId,
|
||||
'entries': entries.map((e) => _cycleEntryToJson(e)).toList(),
|
||||
};
|
||||
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(payload),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
debugPrint('Sync Push Successful');
|
||||
} else {
|
||||
debugPrint('Sync Push Failed: ${response.body}');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Sync Push Error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Pull data from backend
|
||||
Future<List<CycleEntry>> pullSyncData(String userId) async {
|
||||
try {
|
||||
final url = Uri.parse('$_baseUrl/sync/pull?userId=$userId');
|
||||
final response = await http.get(url);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
final List entriesJson = data['entries'] ?? [];
|
||||
return entriesJson.map((json) => _jsonToCycleEntry(json)).toList();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Sync Pull Error: $e');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Helpers (Adapters)
|
||||
Map<String, dynamic> _cycleEntryToJson(CycleEntry entry) {
|
||||
// Convert boolean symptoms to list of strings
|
||||
final symptomsList = <String>[];
|
||||
if (entry.hasHeadache) symptomsList.add('headache');
|
||||
if (entry.hasBloating) symptomsList.add('bloating');
|
||||
if (entry.hasBreastTenderness) symptomsList.add('breastTenderness');
|
||||
if (entry.hasFatigue) symptomsList.add('fatigue');
|
||||
if (entry.hasAcne) symptomsList.add('acne');
|
||||
if (entry.hasLowerBackPain) symptomsList.add('lowerBackPain');
|
||||
if (entry.hasConstipation) symptomsList.add('constipation');
|
||||
if (entry.hasDiarrhea) symptomsList.add('diarrhea');
|
||||
if (entry.hasInsomnia) symptomsList.add('insomnia');
|
||||
|
||||
return {
|
||||
'id': entry.id,
|
||||
'date': entry.date.toIso8601String(),
|
||||
'flowIntensity': entry.flowIntensity?.name,
|
||||
'isPeriodDay': entry.isPeriodDay,
|
||||
'symptoms': jsonEncode(symptomsList),
|
||||
'moods': jsonEncode(entry.mood != null ? [entry.mood!.name] : []),
|
||||
'notes': entry.notes,
|
||||
'createdAt': entry.createdAt.toIso8601String(),
|
||||
'updatedAt': entry.updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
CycleEntry _jsonToCycleEntry(Map<String, dynamic> json) {
|
||||
// FlowIntensity enum parsing
|
||||
FlowIntensity? flow;
|
||||
if (json['flowIntensity'] != null) {
|
||||
flow = FlowIntensity.values.firstWhere(
|
||||
(e) => e.name == json['flowIntensity'],
|
||||
orElse: () => FlowIntensity.medium,
|
||||
);
|
||||
}
|
||||
|
||||
// Mood parsing
|
||||
MoodLevel? mood;
|
||||
final moodsList = _parseList(json['moods']);
|
||||
if (moodsList.isNotEmpty) {
|
||||
try {
|
||||
mood = MoodLevel.values.firstWhere((e) => e.name == moodsList.first);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
final symptoms = _parseList(json['symptoms']);
|
||||
|
||||
return CycleEntry(
|
||||
id: json['id'],
|
||||
date: DateTime.parse(json['date']),
|
||||
flowIntensity: flow,
|
||||
isPeriodDay: json['isPeriodDay'] == true,
|
||||
mood: mood,
|
||||
hasHeadache: symptoms.contains('headache'),
|
||||
hasBloating: symptoms.contains('bloating'),
|
||||
hasBreastTenderness: symptoms.contains('breastTenderness'),
|
||||
hasFatigue: symptoms.contains('fatigue'),
|
||||
hasAcne: symptoms.contains('acne'),
|
||||
hasLowerBackPain: symptoms.contains('lowerBackPain'),
|
||||
hasConstipation: symptoms.contains('constipation'),
|
||||
hasDiarrhea: symptoms.contains('diarrhea'),
|
||||
hasInsomnia: symptoms.contains('insomnia'),
|
||||
notes: json['notes'],
|
||||
createdAt: json['createdAt'] != null
|
||||
? DateTime.parse(json['createdAt'])
|
||||
: DateTime.now(),
|
||||
updatedAt: json['updatedAt'] != null
|
||||
? DateTime.parse(json['updatedAt'])
|
||||
: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
List<String> _parseList(dynamic jsonVal) {
|
||||
if (jsonVal == null) return [];
|
||||
if (jsonVal is String) {
|
||||
try {
|
||||
return List<String>.from(jsonDecode(jsonVal));
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return List<String>.from(jsonVal);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user