feat(report): accomplishment report dialog + entry points

Add AccomplishmentReportDialog with programmer/admin modes, date-range
filter (default this month), and include-unapproved toggle. Wire into
DaySheetsTab app-bar action, ApprovalsTab app-bar action, and
ProgrammerDrillDownScreen app-bar button.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-09-28 19:09:09 +08:00
parent 167e208ffa
commit 1101fb0a97
5 changed files with 489 additions and 1 deletions
+170
View File
@@ -0,0 +1,170 @@
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/providers/profile_provider.dart';
import 'package:tasq/screens/programmer_tasks/report/accomplishment_report_dialog.dart';
import 'package:tasq/theme/app_theme.dart';
import 'package:tasq/utils/app_time.dart';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
Widget _buildApp(Widget child, {List<Override> overrides = const []}) {
return ProviderScope(
overrides: overrides,
child: MaterialApp(
theme: AppTheme.light(),
home: Scaffold(body: child),
),
);
}
/// Opener widget that shows the dialog on tap.
class _DialogOpener extends ConsumerWidget {
const _DialogOpener({this.programmerId});
final String? programmerId;
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () => showAccomplishmentReportDialog(
context,
ref,
programmerId: programmerId,
),
child: const Text('Open'),
);
}
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
void main() {
setUpAll(AppTime.initialize);
// ── Programmer mode (not admin, no programmerId) ─────────────────────────
group('as programmer (not admin)', () {
late List<Override> overrides;
setUp(() {
overrides = [
isStrictAdminProvider.overrideWithValue(false),
profilesProvider.overrideWith(
(ref) => Stream.value([
Profile(id: 'prog-1', role: 'programmer', fullName: 'Alice'),
]),
),
currentUserIdProvider.overrideWithValue('prog-1'),
];
});
testWidgets('shows Accomplishment Report title', (tester) async {
await tester
.pumpWidget(_buildApp(const _DialogOpener(), overrides: overrides));
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
expect(find.text('Accomplishment Report'), findsOneWidget);
});
testWidgets('does not show programmer picker', (tester) async {
await tester
.pumpWidget(_buildApp(const _DialogOpener(), overrides: overrides));
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
expect(find.byType(DropdownButton<Profile>), findsNothing);
});
testWidgets('toggle Include unapproved days is present and off by default',
(tester) async {
await tester
.pumpWidget(_buildApp(const _DialogOpener(), overrides: overrides));
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
expect(find.text('Include unapproved days (marked)'), findsOneWidget);
final sw = tester.widget<Switch>(find.byType(Switch).first);
expect(sw.value, isFalse);
});
testWidgets('date filter defaults to This Month', (tester) async {
await tester
.pumpWidget(_buildApp(const _DialogOpener(), overrides: overrides));
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
expect(find.text('This Month'), findsOneWidget);
});
});
// ── Admin picker mode (isStrictAdmin, no programmerId passed) ────────────
group('as admin (no programmerId, strict admin)', () {
final prog1 = Profile(id: 'prog-1', role: 'programmer', fullName: 'Alice');
final prog2 = Profile(id: 'prog-2', role: 'programmer', fullName: 'Bob');
final admin1 =
Profile(id: 'admin-1', role: 'admin', fullName: 'Admin User');
late List<Override> overrides;
setUp(() {
overrides = [
isStrictAdminProvider.overrideWithValue(true),
profilesProvider.overrideWith(
(ref) => Stream.value([prog1, prog2, admin1]),
),
currentUserIdProvider.overrideWithValue('admin-1'),
];
});
testWidgets('shows programmer picker', (tester) async {
await tester
.pumpWidget(_buildApp(const _DialogOpener(), overrides: overrides));
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
expect(find.byType(DropdownButton<Profile>), findsOneWidget);
});
testWidgets('Generate is disabled until a programmer is chosen',
(tester) async {
await tester
.pumpWidget(_buildApp(const _DialogOpener(), overrides: overrides));
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
final button = tester.widget<FilledButton>(
find.ancestor(
of: find.text('Generate'),
matching: find.byType(FilledButton),
),
);
expect(button.onPressed, isNull);
});
testWidgets('picker shows only programmer-role profiles', (tester) async {
await tester
.pumpWidget(_buildApp(const _DialogOpener(), overrides: overrides));
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
// Open the dropdown to expose its items.
await tester.tap(find.byType(DropdownButton<Profile>));
await tester.pumpAndSettle();
// Programmer names should appear in the dropdown overlay.
expect(find.text('Alice'), findsWidgets);
expect(find.text('Bob'), findsWidgets);
// Admin-role profile must not appear.
expect(find.text('Admin User'), findsNothing);
});
});
}