feat(programmer): dashboard stat tiles + badges

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>
This commit is contained in:
2026-09-27 22:13:43 +08:00
parent 0c99ef270d
commit a7fd1e74f1
4 changed files with 331 additions and 1 deletions
@@ -2,12 +2,20 @@ 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
@@ -33,10 +41,49 @@ class ProgrammerDashboard extends ConsumerWidget {
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 =
@@ -67,7 +114,10 @@ class ProgrammerDashboard extends ConsumerWidget {
readOnly: readOnly,
),
const SizedBox(height: 16),
// TODO: Task 8 inserts StatTiles + BadgesStrip here
StatTiles(stats: stats, showSheetStats: showSheetStats),
const SizedBox(height: 8),
BadgesStrip(badges: badges),
const SizedBox(height: 16),
TaskSections(
groups: groups,
profileById: profileById,