- 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)
283 lines
8.0 KiB
Dart
283 lines
8.0 KiB
Dart
import 'dart:io';
|
|
import 'package:sqlite3/sqlite3.dart' as sql;
|
|
import 'package:path/path.dart' as p;
|
|
|
|
class TrackerDatabase {
|
|
late final sql.Database _db;
|
|
|
|
TrackerDatabase() {
|
|
_init();
|
|
}
|
|
|
|
void _init() {
|
|
// Ensure data directory exists
|
|
final dataDir = Directory('data');
|
|
if (!dataDir.existsSync()) {
|
|
dataDir.createSync();
|
|
}
|
|
|
|
final dbPath = p.join('data', 'tracker.db');
|
|
print('Opening database at $dbPath');
|
|
_db = sql.sqlite3.open(dbPath);
|
|
|
|
_createTables();
|
|
}
|
|
|
|
void _createTables() {
|
|
print('Creating tables if not exist...');
|
|
|
|
// Users table
|
|
_db.execute('''
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id TEXT PRIMARY KEY,
|
|
role TEXT NOT NULL,
|
|
name TEXT,
|
|
email TEXT,
|
|
partner_id TEXT,
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
''');
|
|
|
|
// Cycle Entries table
|
|
_db.execute('''
|
|
CREATE TABLE IF NOT EXISTS cycle_entries (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL,
|
|
date TEXT NOT NULL,
|
|
flow_intensity TEXT,
|
|
is_period_day INTEGER DEFAULT 0,
|
|
symptoms TEXT, -- JSON string
|
|
moods TEXT, -- JSON string
|
|
notes TEXT,
|
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(user_id, date)
|
|
)
|
|
''');
|
|
|
|
// Teaching Plans table
|
|
_db.execute('''
|
|
CREATE TABLE IF NOT EXISTS teaching_plans (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL,
|
|
date TEXT NOT NULL,
|
|
topic TEXT,
|
|
scripture_reference TEXT,
|
|
notes TEXT,
|
|
is_completed INTEGER DEFAULT 0,
|
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
''');
|
|
|
|
// Prayer Requests table
|
|
_db.execute('''
|
|
CREATE TABLE IF NOT EXISTS prayer_requests (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL,
|
|
request TEXT,
|
|
is_answered INTEGER DEFAULT 0,
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
''');
|
|
|
|
print('Tables created.');
|
|
}
|
|
|
|
// Basic CRUD placeholders for sync
|
|
|
|
void upsertCycleEntry(String userId, Map<String, dynamic> entry) {
|
|
// Assuming entry contains fields matching DB
|
|
final stmt = _db.prepare('''
|
|
INSERT OR REPLACE INTO cycle_entries (
|
|
id, user_id, date, flow_intensity, is_period_day, symptoms, moods, notes, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
''');
|
|
|
|
stmt.execute([
|
|
entry['id'],
|
|
userId,
|
|
entry['date'],
|
|
entry['flowIntensity'],
|
|
entry['isPeriodDay'] == true ? 1 : 0,
|
|
entry['symptoms'], // JSON
|
|
entry['moods'], // JSON
|
|
entry['notes'],
|
|
]);
|
|
stmt.dispose();
|
|
}
|
|
|
|
List<Map<String, dynamic>> getCycleEntries(String userId, {String? since}) {
|
|
// If since is provided, filter by updated_at
|
|
// For MVP sync, we might just return all or a simple diff
|
|
final result =
|
|
_db.select('SELECT * FROM cycle_entries WHERE user_id = ?', [userId]);
|
|
return result
|
|
.map((row) => {
|
|
'id': row['id'],
|
|
'date': row['date'],
|
|
'flowIntensity': row['flow_intensity'],
|
|
'isPeriodDay': row['is_period_day'] == 1,
|
|
'symptoms': row['symptoms'],
|
|
'moods': row['moods'],
|
|
'notes': row['notes'],
|
|
'updatedAt': row['updated_at']
|
|
})
|
|
.toList();
|
|
}
|
|
|
|
// Teaching Plan operations
|
|
void upsertTeachingPlan(String userId, Map<String, dynamic> plan) {
|
|
final stmt = _db.prepare('''
|
|
INSERT OR REPLACE INTO teaching_plans (
|
|
id, user_id, date, topic, scripture_reference, notes, is_completed, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
''');
|
|
|
|
stmt.execute([
|
|
plan['id'],
|
|
userId,
|
|
plan['date'],
|
|
plan['topic'],
|
|
plan['scriptureReference'],
|
|
plan['notes'],
|
|
plan['isCompleted'] == true ? 1 : 0,
|
|
]);
|
|
stmt.dispose();
|
|
}
|
|
|
|
List<Map<String, dynamic>> getTeachingPlans(String userId) {
|
|
final result =
|
|
_db.select('SELECT * FROM teaching_plans WHERE user_id = ?', [userId]);
|
|
return result
|
|
.map((row) => {
|
|
'id': row['id'],
|
|
'date': row['date'],
|
|
'topic': row['topic'],
|
|
'scriptureReference': row['scripture_reference'],
|
|
'notes': row['notes'],
|
|
'isCompleted': row['is_completed'] == 1,
|
|
'updatedAt': row['updated_at']
|
|
})
|
|
.toList();
|
|
}
|
|
|
|
// Prayer Request operations
|
|
void upsertPrayerRequest(String userId, Map<String, dynamic> request) {
|
|
final stmt = _db.prepare('''
|
|
INSERT OR REPLACE INTO prayer_requests (
|
|
id, user_id, request, is_answered, created_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
''');
|
|
|
|
stmt.execute([
|
|
request['id'],
|
|
userId,
|
|
request['request'],
|
|
request['isAnswered'] == true ? 1 : 0,
|
|
request['createdAt'],
|
|
]);
|
|
stmt.dispose();
|
|
}
|
|
|
|
List<Map<String, dynamic>> getPrayerRequests(String userId) {
|
|
final result =
|
|
_db.select('SELECT * FROM prayer_requests WHERE user_id = ?', [userId]);
|
|
return result
|
|
.map((row) => {
|
|
'id': row['id'],
|
|
'request': row['request'],
|
|
'isAnswered': row['is_answered'] == 1,
|
|
'createdAt': row['created_at'],
|
|
'updatedAt': row['updated_at']
|
|
})
|
|
.toList();
|
|
}
|
|
|
|
// User operations
|
|
void upsertUser(String userId, Map<String, dynamic> userData) {
|
|
// Check if user exists first
|
|
final existing = _db.select('SELECT * FROM users WHERE id = ?', [userId]);
|
|
|
|
if (existing.isEmpty) {
|
|
// Insert new
|
|
final stmt = _db.prepare('''
|
|
INSERT INTO users (
|
|
id, role, name, email, partner_id, created_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
''');
|
|
stmt.execute([
|
|
userId,
|
|
userData['role'] ?? 'wife',
|
|
userData['name'],
|
|
userData['email'],
|
|
userData['partnerId'],
|
|
userData['createdAt'] ?? DateTime.now().toIso8601String(),
|
|
]);
|
|
stmt.dispose();
|
|
} else {
|
|
// Update existing
|
|
// CRITICAL: Do NOT overwrite partner_id with NULL if it is already set in DB
|
|
// unless the client specifically might intend it (which is hard to know).
|
|
// For Safe Onboarding: If DB has a partner_id, and incoming is NULL, keep DB value.
|
|
|
|
final row = existing.first;
|
|
final dbPartnerId = row['partner_id'];
|
|
final incomingPartnerId = userData['partnerId'];
|
|
|
|
String? finalPartnerId = incomingPartnerId;
|
|
if (incomingPartnerId == null && dbPartnerId != null) {
|
|
finalPartnerId = dbPartnerId as String?;
|
|
}
|
|
|
|
final stmt = _db.prepare('''
|
|
UPDATE users SET
|
|
role = ?,
|
|
name = ?,
|
|
email = ?,
|
|
partner_id = ?,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
''');
|
|
stmt.execute([
|
|
userData['role'] ?? row['role'],
|
|
userData['name'] ?? row['name'],
|
|
userData['email'] ?? row['email'],
|
|
finalPartnerId,
|
|
userId,
|
|
]);
|
|
stmt.dispose();
|
|
}
|
|
}
|
|
|
|
bool linkPartners(String userId, String targetPartnerId) {
|
|
// 1. Verify target exists (optional, or just blindly update)
|
|
// For MVP, just update both.
|
|
|
|
// Update User -> Partner
|
|
_db.execute('UPDATE users SET partner_id = ? WHERE id = ?',
|
|
[targetPartnerId, userId]);
|
|
|
|
// Update Partner -> User
|
|
_db.execute('UPDATE users SET partner_id = ? WHERE id = ?',
|
|
[userId, targetPartnerId]);
|
|
|
|
// Check if both have partner_id set now?
|
|
// Just return true.
|
|
return true;
|
|
}
|
|
|
|
Map<String, dynamic>? getUser(String userId) {
|
|
final result = _db.select('SELECT * FROM users WHERE id = ?', [userId]);
|
|
if (result.isEmpty) return null;
|
|
final row = result.first;
|
|
return {
|
|
'id': row['id'],
|
|
'role': row['role'],
|
|
'name': row['name'],
|
|
'email': row['email'],
|
|
'partnerId': row['partner_id'],
|
|
};
|
|
}
|
|
}
|