Worklog, anti geo spoofing and UI Polish

This commit is contained in:
2026-05-05 05:51:19 +08:00
parent d068887354
commit ccedf6e5f0
15 changed files with 2589 additions and 45 deletions
+64 -4
View File
@@ -328,9 +328,10 @@ final tasksProvider = StreamProvider<List<Task>>((ref) {
onOfflineData: () async {
if (!AppRepository.isConfigured) return [];
try {
return await AppRepository.instance.get<Task>(
final all = await AppRepository.instance.get<Task>(
policy: OfflineFirstGetPolicy.localOnly,
);
return {for (final t in all) t.id: t}.values.toList();
} catch (_) {
return [];
}
@@ -363,6 +364,7 @@ final tasksProvider = StreamProvider<List<Task>>((ref) {
'[tasksProvider] reconnected — syncing ${pending.length} pending task(s)',
);
}
final failedTaskIds = <String>{};
for (final task in pending) {
try {
final syncPayload = <String, dynamic>{
@@ -393,18 +395,26 @@ final tasksProvider = StreamProvider<List<Task>>((ref) {
} catch (e) {
final msg = e.toString();
if (msg.contains('23505') || msg.contains('duplicate key')) {
// Task already in DB (Brick may have synced it first) — safe to
// leave cleanup to the realtime stream listener in tasks_list_screen.
// Task already in DB — treat as success so it's cleared from pending.
debugPrint(
'[tasksProvider] pending task already in DB id=${task.id}',
);
} else {
failedTaskIds.add(task.id);
debugPrint(
'[tasksProvider] failed to sync pending task id=${task.id}: $e',
);
}
}
}
// Clear successfully synced pending tasks. Don't rely on the
// tasks_list_screen listener alone — it only fires when that screen is
// active, leaving the banner stuck on other screens indefinitely.
if (pending.isNotEmpty) {
final remaining =
pending.where((t) => failedTaskIds.contains(t.id)).toList();
ref.read(offlinePendingTasksProvider.notifier).state = remaining;
}
// ── 2. Sync offline field edits for EXISTING tasks (PATCH) ──────────
// pendingUpdates already had pending-task entries removed in step 1.
@@ -573,9 +583,10 @@ final tasksProvider = StreamProvider<List<Task>>((ref) {
// When offline, seed from Brick local SQLite cache instead of network.
if (!ref.read(isOnlineProvider) && AppRepository.isConfigured) {
try {
final cached = await AppRepository.instance.get<Task>(
final all = await AppRepository.instance.get<Task>(
policy: OfflineFirstGetPolicy.localOnly,
);
final cached = {for (final t in all) t.id: t}.values.toList();
debugPrint('[tasksProvider] offline seed: ${cached.length} tasks from local cache');
final payload = _buildTaskPayload(
tasks: cached,
@@ -2788,3 +2799,52 @@ class TaskAssignmentsController {
ref.read(offlinePendingAssignmentsProvider.notifier).state = current;
}
}
// ─── Work Log helpers ────────────────────────────────────────────────────────
/// All task_assignments rows where user_id = [userId].
/// Used by the Work Log tab to identify which tasks the user is the assignee of.
final taskAssignmentsForUserProvider =
FutureProvider.autoDispose.family<List<TaskAssignment>, String>(
(ref, userId) async {
try {
final data = await Supabase.instance.client
.from('task_assignments')
.select()
.eq('user_id', userId);
return data.map(TaskAssignment.fromMap).toList();
} catch (_) {
return [];
}
},
);
/// All task_activity_logs rows for the given task IDs — single batch query.
/// Key is a sorted, comma-joined string of task IDs so Riverpod caches the
/// provider correctly across rebuilds (List equality is reference-based in Dart).
/// Returns logs in DESCENDING order (newest first).
final taskActivityLogsForTasksProvider =
FutureProvider.autoDispose.family<List<TaskActivityLog>, String>(
(ref, taskIdsKey) async {
if (taskIdsKey.isEmpty) return [];
final taskIds = taskIdsKey.split(',');
try {
final data = await Supabase.instance.client
.from('task_activity_logs')
.select()
.inFilter('task_id', taskIds)
.order('created_at', ascending: false);
return data.map(TaskActivityLog.fromMap).toList();
} catch (_) {
try {
final all = await cachedListFromBrick<TaskActivityLog>();
final idSet = taskIds.toSet();
final filtered = all.where((l) => idSet.contains(l.taskId)).toList();
filtered.sort((a, b) => b.createdAt.compareTo(a.createdAt));
return filtered;
} catch (_) {
return [];
}
}
},
);