Files
focusflow_api/lib/src/modules/streaks/streak_repository.dart
Oracle Public Cloud User 8958455a12 Initial scaffold: FocusFlow ADHD Task Manager backend
Dart Shelf API with modules: auth (JWT + PBKDF2), tasks (CRUD + dopamine scorer),
streaks (forgiveness + freeze), rewards (variable reward engine), time perception,
sync (offline-first push/pull), rooms (body doubling placeholder).
Includes DB migration (001_initial_schema.sql) and Docker Compose.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:53:40 +00:00

161 lines
5.1 KiB
Dart

import '../../config/database.dart';
/// Data access layer for streaks and streak entries.
class StreakRepository {
// ── Create ──────────────────────────────────────────────────────────
Future<Map<String, dynamic>> create(Map<String, dynamic> data) async {
final result = await Database.query(
'''
INSERT INTO streaks (
id, user_id, name, description, frequency, grace_days,
current_count, longest_count, frozen_until,
created_at, updated_at
) VALUES (
@id, @user_id, @name, @description, @frequency, @grace_days,
0, 0, NULL,
NOW(), NOW()
)
RETURNING *
''',
parameters: data,
);
return _rowToMap(result.first);
}
// ── Read ────────────────────────────────────────────────────────────
Future<Map<String, dynamic>?> findById(String id, String userId) async {
final result = await Database.query(
'''
SELECT * FROM streaks
WHERE id = @id AND user_id = @user_id
''',
parameters: {'id': id, 'user_id': userId},
);
if (result.isEmpty) return null;
return _rowToMap(result.first);
}
Future<List<Map<String, dynamic>>> findAll(String userId) async {
final result = await Database.query(
'''
SELECT * FROM streaks
WHERE user_id = @user_id
ORDER BY created_at DESC
''',
parameters: {'user_id': userId},
);
return result.map(_rowToMap).toList();
}
// ── Update ──────────────────────────────────────────────────────────
Future<Map<String, dynamic>?> update(
String id,
String userId,
Map<String, dynamic> data,
) async {
final setClauses = <String>[];
final params = <String, dynamic>{'id': id, 'user_id': userId};
data.forEach((key, value) {
setClauses.add('$key = @$key');
params[key] = value;
});
setClauses.add('updated_at = NOW()');
final result = await Database.query(
'''
UPDATE streaks SET ${setClauses.join(', ')}
WHERE id = @id AND user_id = @user_id
RETURNING *
''',
parameters: params,
);
if (result.isEmpty) return null;
return _rowToMap(result.first);
}
// ── Entries ─────────────────────────────────────────────────────────
Future<void> addEntry(Map<String, dynamic> data) async {
await Database.query(
'''
INSERT INTO streak_entries (id, streak_id, entry_date, entry_type, note, created_at)
VALUES (@id, @streak_id, @entry_date, @entry_type, @note, NOW())
ON CONFLICT (streak_id, entry_date) DO NOTHING
''',
parameters: data,
);
}
Future<List<Map<String, dynamic>>> getHistory(
String streakId,
String userId,
) async {
final result = await Database.query(
'''
SELECT se.* FROM streak_entries se
JOIN streaks s ON se.streak_id = s.id
WHERE se.streak_id = @streak_id AND s.user_id = @user_id
ORDER BY se.entry_date DESC
LIMIT 90
''',
parameters: {'streak_id': streakId, 'user_id': userId},
);
return result
.map((row) => <String, dynamic>{
'id': row[0],
'streak_id': row[1],
'entry_date': row[2] is DateTime
? (row[2] as DateTime).toIso8601String()
: row[2],
'entry_type': row[3],
'note': row[4],
'created_at': row[5] is DateTime
? (row[5] as DateTime).toIso8601String()
: row[5],
})
.toList();
}
Future<DateTime?> lastCompletionDate(String streakId) async {
final result = await Database.query(
'''
SELECT MAX(entry_date) FROM streak_entries
WHERE streak_id = @streak_id AND entry_type = 'completion'
''',
parameters: {'streak_id': streakId},
);
if (result.isEmpty || result.first[0] == null) return null;
final val = result.first[0];
if (val is DateTime) return val;
return DateTime.tryParse(val.toString());
}
// ── Row mapper ──────────────────────────────────────────────────────
Map<String, dynamic> _rowToMap(dynamic row) {
final columns = [
'id',
'user_id',
'name',
'description',
'frequency',
'grace_days',
'current_count',
'longest_count',
'frozen_until',
'created_at',
'updated_at',
];
final map = <String, dynamic>{};
for (var i = 0; i < columns.length; i++) {
final value = row[i];
map[columns[i]] = value is DateTime ? value.toIso8601String() : value;
}
return map;
}
}