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:
@@ -0,0 +1,148 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../utils/programmer_stats.dart';
|
||||
import '../../../utils/programmer_task_ledger.dart';
|
||||
import '../../../widgets/app_metric_card.dart';
|
||||
|
||||
/// A vertical list of KPI stat tiles for the programmer dashboard.
|
||||
///
|
||||
/// When [showSheetStats] is false only the first two tiles (today's focus and
|
||||
/// completed-this-week) are shown; sheet-based tiles (streak, first-pass
|
||||
/// approval, help given) are hidden and badges are computed without sheet data.
|
||||
class StatTiles extends StatelessWidget {
|
||||
const StatTiles({
|
||||
super.key,
|
||||
required this.stats,
|
||||
required this.showSheetStats,
|
||||
});
|
||||
|
||||
final ProgrammerStats stats;
|
||||
final bool showSheetStats;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final tiles = <Widget>[
|
||||
_buildFocusTile(context, cs),
|
||||
const SizedBox(height: 8),
|
||||
_buildCompletedTile(context),
|
||||
];
|
||||
if (showSheetStats) {
|
||||
tiles.addAll([
|
||||
const SizedBox(height: 8),
|
||||
_buildStreakTile(context),
|
||||
const SizedBox(height: 8),
|
||||
_buildFirstPassTile(context),
|
||||
const SizedBox(height: 8),
|
||||
_buildHelpGivenTile(context),
|
||||
]);
|
||||
}
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: tiles,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFocusTile(BuildContext context, ColorScheme cs) {
|
||||
final progressValue =
|
||||
math.min(1.0, stats.todayFocus.inSeconds / (8 * 3600));
|
||||
final trailing = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: CircularProgressIndicator(
|
||||
value: progressValue,
|
||||
strokeWidth: 6,
|
||||
backgroundColor: cs.surfaceContainerHighest,
|
||||
color: cs.primary,
|
||||
strokeCap: StrokeCap.round,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'/ 8h',
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
return AppMetricCard(
|
||||
title: "TODAY'S FOCUS",
|
||||
value: formatDurationHm(stats.todayFocus),
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompletedTile(BuildContext context) {
|
||||
final diff = stats.completedThisWeek - stats.completedLastWeek;
|
||||
final String trailingText;
|
||||
if (diff > 0) {
|
||||
trailingText = '↑$diff vs last week';
|
||||
} else if (diff < 0) {
|
||||
trailingText = '↓${-diff} vs last week';
|
||||
} else {
|
||||
trailingText = 'Same as last week';
|
||||
}
|
||||
return AppMetricCard(
|
||||
title: 'COMPLETED THIS WEEK',
|
||||
value: stats.completedThisWeek.toString(),
|
||||
trailing: Text(
|
||||
trailingText,
|
||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStreakTile(BuildContext context) {
|
||||
final n = stats.currentStreak;
|
||||
final dayLabel = n == 1 ? '1 day' : '$n days';
|
||||
return AppMetricCard(
|
||||
title: 'STREAK',
|
||||
value: dayLabel,
|
||||
icon: Icons.local_fire_department_outlined,
|
||||
trailing: Text(
|
||||
'Best ${stats.bestStreak}',
|
||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFirstPassTile(BuildContext context) {
|
||||
final value = stats.firstPassRate != null
|
||||
? '${(stats.firstPassRate! * 100).round()}%'
|
||||
: '—';
|
||||
return AppMetricCard(
|
||||
title: 'FIRST-PASS APPROVAL',
|
||||
value: value,
|
||||
trailing: Text(
|
||||
'Last 30 days',
|
||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHelpGivenTile(BuildContext context) {
|
||||
return AppMetricCard(
|
||||
title: 'HELP GIVEN',
|
||||
value: formatDurationHm(stats.helpGivenThisMonth),
|
||||
trailing: Text(
|
||||
'This month',
|
||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user