Files
redz1029 2946945ce3 fix(programmer-tasks): tab switching + help-given live data
Tab switching: _buildTabs was recreating TabController on every build
because `_tabs == tabs` compares List by reference identity, not content.
programmerTasksTabsFor() always returns a new list, so the guard was
always false, resetting the controller to initialIndex 0 on every tap.
Fixed by using listEquals from flutter/foundation.

Help given: computeProgrammerStats accumulated helpGivenThisMonth only
from approvedSnapshot rows, which only exist after admin approval.
Programmers who helped others but had pending sheets saw 0m indefinitely.
Fixed by adding helpGivenThisMonthProvider (FutureProvider.family) that
queries programmer_task_work_logs directly for the current Manila month,
and passing it as liveHelpThisMonthSec to computeProgrammerStats.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-09-28 22:25:25 +08:00

176 lines
5.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../models/profile.model.dart';
import '../../../models/programmer_day_sheet.model.dart';
import '../../../models/programmer_task.model.dart';
import '../../../providers/leave_provider.dart';
import '../../../providers/profile_provider.dart';
import '../../../providers/programmer_day_data_provider.dart';
import '../../../providers/programmer_day_sheets_provider.dart';
import '../../../providers/programmer_tasks_provider.dart';
import '../../../utils/app_time.dart';
import '../../../utils/programmer_daily_time.dart';
import '../../../utils/programmer_day_rows.dart';
import '../../../utils/programmer_stats.dart';
import '../../../utils/staff_presence.dart';
import 'badges_strip.dart';
import 'now_working_card.dart';
import 'stat_tiles.dart';
import 'task_sections.dart';
/// The programmer's own work dashboard — hero card, stat tiles (Task 8), and
/// task sections. Drop it into the Mine tab with `userId: me`.
class ProgrammerDashboard extends ConsumerWidget {
const ProgrammerDashboard({
super.key,
required this.userId,
this.readOnly = false,
});
final String userId;
final bool readOnly;
@override
Widget build(BuildContext context, WidgetRef ref) {
// Drive 30-second rebuilds so duration values stay fresh.
ref.watch(clockTickerProvider);
final tasksAsync = ref.watch(programmerTasksProvider);
final runStates = ref.watch(programmerRunStatesProvider);
final todayKey = ref.watch(todayKeyProvider);
final dayDataAsync = ref.watch(programmerDayDataProvider(todayKey));
final profilesAsync = ref.watch(profilesProvider);
final now = ref.watch(clockTickerProvider).valueOrNull ?? AppTime.now();
final sheetsAsync = ref.watch(daySheetsForUserProvider(userId));
final sheets = sheetsAsync.valueOrNull ?? const <ProgrammerDaySheet>[];
final liveHelpThisMonthSec =
ref.watch(helpGivenThisMonthProvider(userId)).valueOrNull;
final tasks = tasksAsync.valueOrNull ?? const [];
final profileById = <String, Profile>{
for (final p in profilesAsync.valueOrNull ?? <Profile>[]) p.id: p,
};
final profileNames = <String, String>{
for (final p in profilesAsync.valueOrNull ?? <Profile>[]) p.id: p.fullName,
};
final dayData = dayDataAsync.valueOrNull;
final leaveDays = approvedLeaveDays(
ref.watch(leavesProvider).valueOrNull ?? [],
userId,
);
final todayRows = dayData == null
? const <DaySheetRow>[]
: buildDayRows(
userId: userId,
day: todayKey,
tasks: tasks,
logsByTask: dayData.logsByTask,
workLogs: dayData.workLogs,
projectNames: const {},
profileNames: profileNames,
now: now,
);
final todayFocus = Duration(seconds: totalSeconds(todayRows));
final stats = computeProgrammerStats(
userId: userId,
now: now,
tasks: tasks,
sheets: sheets,
leaveDays: leaveDays,
todayFocus: todayFocus,
liveHelpThisMonthSec: liveHelpThisMonthSec,
);
final showSheetStats =
ref.watch(currentProfileProvider).valueOrNull?.role == 'programmer';
final badges = computeBadges(stats, includeSheetBadges: showSheetStats);
final groups = groupTasksForDashboard(tasks, runStates, userId);
final runningTask =
groups.running.isNotEmpty ? groups.running.first : null;
final nextUp =
groups.notStarted.isNotEmpty ? groups.notStarted.first : null;
final todayOnTask = _computeTodayDuration(
runningTask: runningTask,
dayDataAsync: dayDataAsync,
userId: userId,
todayKey: todayKey,
);
final totalOnTask = _computeTotalDuration(
runningTask: runningTask,
dayDataAsync: dayDataAsync,
userId: userId,
);
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
children: [
NowWorkingCard(
running: runningTask,
nextUp: nextUp,
todayOnTask: todayOnTask,
totalOnTask: totalOnTask,
readOnly: readOnly,
),
const SizedBox(height: 16),
StatTiles(stats: stats, showSheetStats: showSheetStats),
const SizedBox(height: 8),
BadgesStrip(badges: badges),
const SizedBox(height: 16),
TaskSections(
groups: groups,
profileById: profileById,
readOnly: readOnly,
),
],
);
}
Duration _computeTodayDuration({
required ProgrammerTask? runningTask,
required AsyncValue<dynamic> dayDataAsync,
required String userId,
required DateTime todayKey,
}) {
if (runningTask == null) return Duration.zero;
final dayData = dayDataAsync.valueOrNull;
if (dayData == null) return Duration.zero;
final logs = dayData.logsByTask[runningTask.id] ?? const [];
final dailyByAssignee = dailySecondsByAssignee(
task: runningTask,
logs: logs,
now: DateTime.now().toUtc(),
);
final secs = dailyByAssignee[userId]?[todayKey] ?? 0;
return Duration(seconds: secs);
}
Duration _computeTotalDuration({
required ProgrammerTask? runningTask,
required AsyncValue<dynamic> dayDataAsync,
required String userId,
}) {
if (runningTask == null) return Duration.zero;
final dayData = dayDataAsync.valueOrNull;
if (dayData == null) return Duration.zero;
final logs = dayData.logsByTask[runningTask.id] ?? const [];
final dailyByAssignee = dailySecondsByAssignee(
task: runningTask,
logs: logs,
now: DateTime.now().toUtc(),
);
final totalSecs =
(dailyByAssignee[userId] ?? const {}).values.fold<int>(0, (s, v) => s + v);
return Duration(seconds: totalSecs);
}
}