2946945ce3
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>
176 lines
5.7 KiB
Dart
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);
|
|
}
|
|
}
|