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>
This commit is contained in:
2026-09-28 18:29:28 +08:00
parent 5af0ce62d9
commit 2585cce36d
5 changed files with 289 additions and 29 deletions
@@ -143,8 +143,16 @@ class _DaySheetDetailScreenState extends ConsumerState<DaySheetDetailScreen> {
final today = ref.read(todayKeyProvider);
final isToday = sheet.workDate == today;
if (isToday) {
// Only warn if THIS programmer has a running task (T20/QN-1 fix).
final allTasks =
ref.read(programmerTasksProvider).valueOrNull ?? const [];
final programmerTaskIds = allTasks
.where((t) => t.assigneeId == sheet.programmerId)
.map((t) => t.id)
.toSet();
final runStates = ref.read(programmerRunStatesProvider);
final hasRunning = runStates.entries.any((e) =>
programmerTaskIds.contains(e.key) &&
e.value.state == ProgrammerRunState.running);
if (hasRunning) {
final confirm = await showDialog<bool>(
@@ -230,10 +238,26 @@ class _DaySheetDetailScreenState extends ConsumerState<DaySheetDetailScreen> {
.where((t) => t.assigneeId == sheet.programmerId)
.toList();
// For simplicity, build live rows from any tasks that appear in events
// or we just use empty — the sheet already has snapshot rows if approved.
// Live rows come from an RPC-built dataset; use snapshot rows if approved.
final liveRows = _buildLiveRowsFrom(sheet, events, allTasks);
// For approved sheets use the stored snapshot. For all other
// statuses fetch live row data from the day-activity provider.
final dayDataForSheet = ref
.watch(programmerDayDataProvider(sheet.workDate))
.valueOrNull;
final liveRows = sheet.status == DaySheetStatus.approved &&
sheet.approvedSnapshot != null
? sheet.approvedSnapshot!.rows
: dayDataForSheet != null
? buildDayRows(
userId: sheet.programmerId,
day: sheet.workDate,
tasks: allTasks,
logsByTask: dayDataForSheet.logsByTask,
workLogs: dayDataForSheet.workLogs,
projectNames: const {},
profileNames: profileNames,
now: AppTime.now(),
)
: const <DaySheetRow>[];
final rows = rowsForSheet(sheet, liveRows);
final totalSecs = rows.fold<int>(0, (s, r) => s + r.seconds);
@@ -475,24 +499,3 @@ class _Header extends StatelessWidget {
}
}
// ---------------------------------------------------------------------------
// Live rows helper
// ---------------------------------------------------------------------------
/// Attempt to surface live rows from the existing tasks list.
/// For a fully approved sheet the snapshot rows will be used anyway,
/// so this is only a best-effort fallback.
List<DaySheetRow> _buildLiveRowsFrom(
ProgrammerDaySheet sheet,
List<ProgrammerDaySheetEvent> events,
List<ProgrammerTask> allTasks,
) {
// If the sheet has an approved snapshot, the caller will use snapshot rows.
if (sheet.approvedSnapshot != null &&
sheet.status == DaySheetStatus.approved) {
return sheet.approvedSnapshot!.rows;
}
// Otherwise we don't have the full row data client-side without a separate
// fetch — return empty and let the screen show whatever the snapshot provides.
return const [];
}