Files
tasq/lib/screens/programmer_tasks/dashboard/stat_tiles.dart
T
redz1029 a7fd1e74f1 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>
2026-09-27 22:13:43 +08:00

149 lines
4.2 KiB
Dart

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,
),
),
);
}
}