import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/programmer_task.model.dart'; import '../models/programmer_task_activity_log.model.dart'; import '../models/programmer_task_work_log.model.dart'; import '../utils/app_time.dart'; import '../utils/programmer_daily_time.dart'; import 'programmer_tasks_provider.dart'; import 'supabase_provider.dart'; enum ProgrammerRunState { running, paused, notStarted, completed, cancelled } typedef ProgrammerRunInfo = ({ProgrammerRunState state, DateTime? since}); /// Pure function — no providers. Map computeRunStates( List tasks, Map> logsByTask, ) { return {for (final t in tasks) t.id: _runInfoFor(t, logsByTask[t.id] ?? [])}; } ProgrammerRunInfo _runInfoFor( ProgrammerTask task, List logs, ) { switch (task.status) { case ProgrammerTaskStatus.queued: return (state: ProgrammerRunState.notStarted, since: null); case ProgrammerTaskStatus.completed: return (state: ProgrammerRunState.completed, since: null); case ProgrammerTaskStatus.cancelled: return (state: ProgrammerRunState.cancelled, since: null); default: return _inProgressInfo(task, logs); } } ProgrammerRunInfo _inProgressInfo( ProgrammerTask task, List logs, ) { const types = ['started', 'paused', 'resumed']; final relevant = logs.where((l) => types.contains(l.actionType)).toList() ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); if (relevant.isEmpty) { return (state: ProgrammerRunState.running, since: task.startedAt); } final latest = relevant.first; if (latest.actionType == 'paused') { return (state: ProgrammerRunState.paused, since: latest.createdAt); } return (state: ProgrammerRunState.running, since: latest.createdAt); } class DayActivityData { const DayActivityData({ required this.day, required this.tasks, required this.logsByTask, required this.workLogs, }); final DateTime day; final List tasks; final Map> logsByTask; final List workLogs; } bool _isTaskActiveOnDay( ProgrammerTask t, DateTime dayStart, DateTime dayEnd, DateTime now, ) { if (t.status == ProgrammerTaskStatus.inProgress) return true; if (t.startedAt == null) return false; final terminal = t.completedAt ?? t.cancelledAt ?? now; return t.startedAt!.isBefore(dayEnd) && !terminal.isBefore(dayStart); } Future> _fetchWorkLogs( dynamic client, DateTime day, ) async { final raw = await client .from('programmer_task_work_logs') .select() .eq('work_date', formatDayKey(day)) as List; return raw .map((r) => ProgrammerTaskWorkLog.fromMap(r as Map)) .toList(); } Future>> _fetchActivityLogs( dynamic client, List ids, ) async { final raw = await client .from('programmer_task_activity_logs') .select() .inFilter('task_id', ids) .order('created_at', ascending: false) as List; final map = >{}; for (final r in raw) { final log = ProgrammerTaskActivityLog.fromMap(r as Map); map.putIfAbsent(log.taskId, () => []).add(log); } return map; } Future fetchDayActivityData( dynamic client, { required DateTime day, required List tasks, required DateTime now, }) async { final dayStart = manilaDayStart(day); final dayEnd = dayStart.add(const Duration(days: 1)); final candidates = tasks.where((t) => _isTaskActiveOnDay(t, dayStart, dayEnd, now)).toList(); final workLogs = await _fetchWorkLogs(client, day); final cIds = {for (final t in candidates) t.id}; candidates.addAll( tasks.where((t) => workLogs.any((l) => l.taskId == t.id) && !cIds.contains(t.id)), ); if (candidates.isEmpty) { return DayActivityData(day: day, tasks: [], logsByTask: {}, workLogs: workLogs); } final logsByTask = await _fetchActivityLogs(client, candidates.map((t) => t.id).toList()); return DayActivityData( day: day, tasks: candidates, logsByTask: logsByTask, workLogs: workLogs, ); } /// Emits [AppTime.now()] immediately then every 30 seconds. final clockTickerProvider = StreamProvider((ref) async* { yield AppTime.now(); yield* Stream.periodic(const Duration(seconds: 30), (_) => AppTime.now()); }); /// Manila calendar day derived from the clock ticker. final todayKeyProvider = Provider((ref) { final tick = ref.watch(clockTickerProvider).valueOrNull; return manilaDay(tick ?? AppTime.now()); }); /// Increments by 1 for each INSERT on either log table. final programmerActivitySignalProvider = StreamProvider((ref) { final client = ref.watch(supabaseClientProvider); final controller = StreamController(); var count = 0; void onInsert(PostgresChangePayload _) { count++; if (!controller.isClosed) controller.add(count); } final channel = client .channel('programmer_activity_signal') .onPostgresChanges( event: PostgresChangeEvent.insert, schema: 'public', table: 'programmer_task_work_logs', callback: onInsert, ) .onPostgresChanges( event: PostgresChangeEvent.insert, schema: 'public', table: 'programmer_task_activity_logs', callback: onInsert, ); channel.subscribe(); ref.onDispose(() { client.removeChannel(channel); controller.close(); }); return controller.stream; }); /// Fetches activity data for a given day key. Invalidated by the activity signal. final programmerDayDataProvider = FutureProvider.family((ref, day) async { ref.watch(programmerActivitySignalProvider); final tasks = ref.watch(programmerTasksProvider).valueOrNull ?? []; final client = ref.watch(supabaseClientProvider); return fetchDayActivityData(client, day: day, tasks: tasks, now: AppTime.now()); }); /// Total help-given seconds for [userId] in the current Manila calendar month. /// /// Queries work logs with minutes > 0 authored by [userId] this month. /// Re-fetches whenever [programmerActivitySignalProvider] fires. final helpGivenThisMonthProvider = FutureProvider.family((ref, userId) async { ref.watch(programmerActivitySignalProvider); final client = ref.watch(supabaseClientProvider); final today = ref.watch(todayKeyProvider); final monthStart = DateTime.utc(today.year, today.month, 1); final nextMonthStart = today.month < 12 ? DateTime.utc(today.year, today.month + 1, 1) : DateTime.utc(today.year + 1, 1, 1); final raw = await client .from('programmer_task_work_logs') .select('minutes') .eq('author_id', userId) .gte('work_date', formatDayKey(monthStart)) .lt('work_date', formatDayKey(nextMonthStart)) as List; int totalSec = 0; for (final r in raw) { final minutes = r['minutes'] as int?; if (minutes != null && minutes > 0) totalSec += minutes * 60; } return totalSec; }); /// Run states for today's tasks, derived from today's [programmerDayDataProvider]. final programmerRunStatesProvider = Provider>((ref) { final today = ref.watch(todayKeyProvider); final data = ref.watch(programmerDayDataProvider(today)).valueOrNull; if (data == null) return {}; return computeRunStates(data.tasks, data.logsByTask); });