Files
tasq/lib/screens/programmer_tasks/dashboard/programmer_dashboard.dart
T
redz1029 2585cce36d feat(programmer): sheet-driven stats, monitor status, drill-down history
- Wire daySheetsForUserProvider(userId) into ProgrammerDashboard so
  computeProgrammerStats/computeBadges receive real sheet data instead of
  the placeholder const <ProgrammerDaySheet>[].
- Monitor tab: watch allDaySheetsProvider and build sheetStatusByUser map
  for today; pass it to buildMonitorEntries; add "Sheets pending" chip
  (pending + disapproved today) to the summary row.
- ProgrammerDrillDownScreen: add _RecentDaySheetsPanel below the dashboard
  showing the last 10 sheets (date + status pill) with tap-to-daySheetDetail.
- DaySheetDetailScreen (T17 gaps): replace const [] liveRows with
  programmerDayDataProvider(sheet.workDate) built rows for non-approved
  sheets; fix _onApprove timer check to filter only the sheet programmer's
  own tasks (QN-1).
- Add test/programmer_drill_down_test.dart (3 tests, all passing).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-09-28 18:29:28 +08:00

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