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
+128
View File
@@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tasq/models/profile.model.dart';
import 'package:tasq/models/programmer_day_sheet.model.dart';
import 'package:tasq/providers/leave_provider.dart';
import 'package:tasq/providers/programmer_day_data_provider.dart';
import 'package:tasq/providers/programmer_day_sheets_provider.dart';
import 'package:tasq/providers/programmer_tasks_provider.dart';
import 'package:tasq/providers/profile_provider.dart';
import 'package:tasq/screens/programmer_tasks/programmer_drill_down_screen.dart';
import 'package:tasq/theme/app_theme.dart';
import 'package:tasq/utils/app_time.dart';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
ProgrammerDaySheet _sheet({
required String id,
required String status,
required DateTime workDate,
}) {
final now = AppTime.now();
return ProgrammerDaySheet(
id: id,
programmerId: 'u1',
status: status,
workDate: workDate,
autoSubmitted: false,
resubmissions: 0,
createdAt: now,
updatedAt: now,
);
}
Widget _buildApp(String userId, {List<Override> overrides = const []}) {
return ProviderScope(
overrides: overrides,
child: MaterialApp(
theme: AppTheme.light(),
home: ProgrammerDrillDownScreen(userId: userId),
),
);
}
List<Override> _minimalOverrides({
List<ProgrammerDaySheet> sheets = const [],
}) {
final today = DateTime.utc(2026, 9, 28);
return [
isStrictAdminProvider.overrideWith((ref) => true),
profilesProvider.overrideWith((ref) => Stream.value([])),
currentProfileProvider.overrideWith((ref) => Stream.value(null)),
programmerTasksProvider.overrideWith((ref) => Stream.value([])),
programmerRunStatesProvider.overrideWith((ref) => const {}),
clockTickerProvider.overrideWith((ref) => Stream.value(today)),
todayKeyProvider.overrideWith((ref) => today),
leavesProvider.overrideWith((ref) => Stream.value([])),
programmerDayDataProvider.overrideWith(
(ref, day) => Future.value(
DayActivityData(day: day, tasks: [], logsByTask: {}, workLogs: []),
),
),
daySheetsForUserProvider.overrideWith((ref, userId) => Stream.value(sheets)),
];
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
void main() {
setUpAll(AppTime.initialize);
group('ProgrammerDrillDownScreen — recent day sheets', () {
testWidgets('shows Recent day sheets header when there are sheets',
(tester) async {
final sheets = [
_sheet(
id: 's1',
status: DaySheetStatus.pending,
workDate: DateTime.utc(2026, 9, 27),
),
_sheet(
id: 's2',
status: DaySheetStatus.approved,
workDate: DateTime.utc(2026, 9, 26),
),
];
await tester.pumpWidget(
_buildApp('u1', overrides: _minimalOverrides(sheets: sheets)),
);
await tester.pump();
// AppSectionHeader renders title in uppercase
expect(find.text('RECENT DAY SHEETS'), findsOneWidget);
});
testWidgets('shows both sheet dates', (tester) async {
final d1 = DateTime.utc(2026, 9, 27);
final d2 = DateTime.utc(2026, 9, 26);
final sheets = [
_sheet(id: 's1', status: DaySheetStatus.pending, workDate: d1),
_sheet(id: 's2', status: DaySheetStatus.approved, workDate: d2),
];
await tester.pumpWidget(
_buildApp('u1', overrides: _minimalOverrides(sheets: sheets)),
);
await tester.pump();
expect(find.text(AppTime.formatDate(d1)), findsOneWidget);
expect(find.text(AppTime.formatDate(d2)), findsOneWidget);
});
testWidgets('shows RECENT DAY SHEETS even with empty sheets list',
(tester) async {
await tester.pumpWidget(
_buildApp('u1', overrides: _minimalOverrides()),
);
await tester.pump();
expect(find.text('RECENT DAY SHEETS'), findsOneWidget);
});
});
}