a7fd1e74f1
Add StatTiles and BadgesStrip widgets wired into ProgrammerDashboard. StatTiles shows 2 tiles (focus + completed) when showSheetStats is false, and 5 tiles (+ streak, first-pass approval, help given) when the viewing user's role is programmer. BadgesStrip renders earned/locked chips in a horizontal Wrap with tap-to-dialog detail. Dashboard feeds todayFocus via buildDayRows/totalSeconds, leaveDays via approvedLeaveDays, and sheets: const [] until Task 20 wires real data. Co-Authored-By: claude-flow <ruv@ruv.net>
168 lines
5.4 KiB
Dart
168 lines
5.4 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_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 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: const <ProgrammerDaySheet>[],
|
|
leaveDays: leaveDays,
|
|
todayFocus: todayFocus,
|
|
);
|
|
|
|
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);
|
|
}
|
|
}
|