feat(programmer): day sheets + approvals tabs
- day_sheet_snapshot_builder.dart: buildSnapshotForSheet (approved → stored snapshot, else live buildDayRows), groupSheetsByDate (descending keys), approvalCounts (pending/awaiting/approved) - approvals_tab.dart: status filter chips, programmer dropdown, ReportDateFilter.controlled, date-grouped cards with Auto/Early/Round badges, checkbox multi-select, bulk Approve selected (n) flow - day_sheets_tab.dart: Today card (live rows, total, status, Submit day), Needs your attention (disapproved), history with AppStatusSummaryRow All/Pending/Disapproved/Approved filter - programmerTasksTabsFor: admin → [monitor, approvals, mine, all]; programmer → [mine, all, daySheets] - programmer_tasks_list_screen.dart: wire approvals + daySheets cases Tests: 15 passing (day_sheet_tabs_test + programmer_tabs_test) Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tasq/models/programmer_day_sheet.model.dart';
|
||||
import 'package:tasq/providers/profile_provider.dart';
|
||||
import 'package:tasq/providers/programmer_day_sheets_provider.dart';
|
||||
import 'package:tasq/screens/programmer_tasks/day_sheets/approvals_tab.dart';
|
||||
import 'package:tasq/screens/programmer_tasks/day_sheets/day_sheet_snapshot_builder.dart';
|
||||
import 'package:tasq/theme/app_theme.dart';
|
||||
import 'package:tasq/utils/app_time.dart';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
ProgrammerDaySheet _sheet({
|
||||
required String id,
|
||||
required String status,
|
||||
DateTime? workDate,
|
||||
bool autoSubmitted = false,
|
||||
int resubmissions = 0,
|
||||
}) {
|
||||
final now = AppTime.now();
|
||||
final date = workDate ?? DateTime.utc(now.year, now.month, 15);
|
||||
return ProgrammerDaySheet(
|
||||
id: id,
|
||||
programmerId: 'prog-1',
|
||||
status: status,
|
||||
workDate: date,
|
||||
autoSubmitted: autoSubmitted,
|
||||
resubmissions: resubmissions,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildApp(Widget child, {List<Override> overrides = const []}) {
|
||||
return ProviderScope(
|
||||
overrides: overrides,
|
||||
child: MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
home: Scaffold(body: child),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void main() {
|
||||
setUpAll(AppTime.initialize);
|
||||
|
||||
// ── groupSheetsByDate ────────────────────────────────────────────────────
|
||||
|
||||
group('groupSheetsByDate', () {
|
||||
test('groups by workDate, keys descending', () {
|
||||
final d1 = DateTime.utc(2024, 1, 3);
|
||||
final d2 = DateTime.utc(2024, 1, 2);
|
||||
final d3 = DateTime.utc(2024, 1, 1);
|
||||
|
||||
final sheets = [
|
||||
_sheet(id: 'a', status: DaySheetStatus.pending, workDate: d2),
|
||||
_sheet(id: 'b', status: DaySheetStatus.approved, workDate: d1),
|
||||
_sheet(id: 'c', status: DaySheetStatus.draft, workDate: d3),
|
||||
_sheet(id: 'd', status: DaySheetStatus.pending, workDate: d2),
|
||||
];
|
||||
|
||||
final grouped = groupSheetsByDate(sheets);
|
||||
final keys = grouped.keys.toList();
|
||||
|
||||
expect(keys, [d1, d2, d3]);
|
||||
expect(grouped[d1]!.map((s) => s.id).toList(), ['b']);
|
||||
expect(grouped[d2]!.map((s) => s.id).toList(), ['a', 'd']);
|
||||
expect(grouped[d3]!.map((s) => s.id).toList(), ['c']);
|
||||
});
|
||||
|
||||
test('single sheet returns single-entry map', () {
|
||||
final d = DateTime.utc(2024, 6, 1);
|
||||
final result = groupSheetsByDate([_sheet(id: 'x', status: DaySheetStatus.pending, workDate: d)]);
|
||||
expect(result.keys.toList(), [d]);
|
||||
expect(result[d]!.length, 1);
|
||||
});
|
||||
|
||||
test('empty list returns empty map', () {
|
||||
expect(groupSheetsByDate([]), isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
// ── approvalCounts ───────────────────────────────────────────────────────
|
||||
|
||||
group('approvalCounts', () {
|
||||
test('counts pending, awaiting (disapproved) and approved', () {
|
||||
final sheets = [
|
||||
_sheet(id: '1', status: DaySheetStatus.pending),
|
||||
_sheet(id: '2', status: DaySheetStatus.pending),
|
||||
_sheet(id: '3', status: DaySheetStatus.disapproved),
|
||||
_sheet(id: '4', status: DaySheetStatus.approved),
|
||||
_sheet(id: '5', status: DaySheetStatus.draft),
|
||||
];
|
||||
|
||||
final counts = approvalCounts(sheets);
|
||||
expect(counts.pending, 2);
|
||||
expect(counts.awaiting, 1);
|
||||
expect(counts.approved, 1);
|
||||
});
|
||||
|
||||
test('all zero for empty list', () {
|
||||
final counts = approvalCounts([]);
|
||||
expect(counts.pending, 0);
|
||||
expect(counts.awaiting, 0);
|
||||
expect(counts.approved, 0);
|
||||
});
|
||||
|
||||
test('draft sheets are not counted', () {
|
||||
final counts = approvalCounts([
|
||||
_sheet(id: '1', status: DaySheetStatus.draft),
|
||||
_sheet(id: '2', status: DaySheetStatus.draft),
|
||||
]);
|
||||
expect(counts.pending, 0);
|
||||
expect(counts.awaiting, 0);
|
||||
expect(counts.approved, 0);
|
||||
});
|
||||
});
|
||||
|
||||
// ── ApprovalsTab widget ──────────────────────────────────────────────────
|
||||
|
||||
group('ApprovalsTab', () {
|
||||
List<ProgrammerDaySheet> _pendingSheets(int n) {
|
||||
final now = AppTime.now();
|
||||
return List.generate(
|
||||
n,
|
||||
(i) => _sheet(
|
||||
id: 'sheet-$i',
|
||||
status: DaySheetStatus.pending,
|
||||
workDate: DateTime.utc(now.year, now.month, 15),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets(
|
||||
'selecting 2 of 3 pending cards shows Approve selected (2)',
|
||||
(tester) async {
|
||||
final sheets = _pendingSheets(3);
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildApp(
|
||||
const ApprovalsTab(),
|
||||
overrides: [
|
||||
allDaySheetsProvider.overrideWith(
|
||||
(ref) => Stream.value(sheets),
|
||||
),
|
||||
profilesProvider.overrideWith(
|
||||
(ref) => Stream.value([]),
|
||||
),
|
||||
isStrictAdminProvider.overrideWith((ref) => true),
|
||||
currentUserIdProvider.overrideWith((ref) => 'admin-1'),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// Let the stream settle.
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
// All 3 pending cards should be visible with checkboxes.
|
||||
final checkboxes = find.byType(Checkbox);
|
||||
expect(checkboxes, findsAtLeastNWidgets(2));
|
||||
|
||||
// Select two cards.
|
||||
await tester.tap(checkboxes.at(0));
|
||||
await tester.pump();
|
||||
await tester.tap(checkboxes.at(1));
|
||||
await tester.pump();
|
||||
|
||||
// The approve button must reflect the selection count.
|
||||
expect(find.text('Approve selected (2)'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -49,18 +49,26 @@ void main() {
|
||||
setUpAll(AppTime.initialize);
|
||||
|
||||
group('programmerTasksTabsFor', () {
|
||||
test('tabs in this slice', () {
|
||||
test('admin gets monitor + approvals + mine + all', () {
|
||||
expect(
|
||||
programmerTasksTabsFor(isStrictAdmin: true),
|
||||
[
|
||||
ProgrammerTasksTab.monitor,
|
||||
ProgrammerTasksTab.approvals,
|
||||
ProgrammerTasksTab.mine,
|
||||
ProgrammerTasksTab.all,
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('programmer gets mine + all + daySheets', () {
|
||||
expect(
|
||||
programmerTasksTabsFor(isStrictAdmin: false),
|
||||
[ProgrammerTasksTab.mine, ProgrammerTasksTab.all],
|
||||
[
|
||||
ProgrammerTasksTab.mine,
|
||||
ProgrammerTasksTab.all,
|
||||
ProgrammerTasksTab.daySheets,
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user