Files
Tracker/backend/lib/database.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

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'],
};
}
}