feat(programmer): Mine dashboard hero + task sections
Replace the Mine tab flat list with ProgrammerDashboard — a NowWorkingCard hero (running task with Pause/Complete, or idle with nextUp Start), a TODO stub for Task 8 stat tiles/badges, and collapsible TaskSections (In progress, Paused, Not started always-visible; Completed/Cancelled start collapsed with Show-all at 10+). groupTasksForDashboard excludes other assignees and sorts completed/cancelled by terminal date desc. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
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_task.model.dart';
|
||||
import 'package:tasq/providers/programmer_day_data_provider.dart';
|
||||
import 'package:tasq/screens/programmer_tasks/dashboard/task_sections.dart';
|
||||
import 'package:tasq/utils/app_time.dart';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
ProgrammerTask _makeTask({
|
||||
required String id,
|
||||
required String status,
|
||||
String assigneeId = 'user1',
|
||||
DateTime? completedAt,
|
||||
DateTime? cancelledAt,
|
||||
}) {
|
||||
final now = DateTime(2026, 1, 1);
|
||||
return ProgrammerTask(
|
||||
id: id,
|
||||
title: 'Task $id',
|
||||
category: ProgrammerTaskCategory.bugFix,
|
||||
status: status,
|
||||
assigneeId: assigneeId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
completedAt: completedAt,
|
||||
cancelledAt: cancelledAt,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSections(TaskSectionGroups groups, {bool readOnly = false}) {
|
||||
return ProviderScope(
|
||||
overrides: [
|
||||
programmerRunStatesProvider.overrideWithValue({}),
|
||||
],
|
||||
child: MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SingleChildScrollView(
|
||||
child: TaskSections(
|
||||
groups: groups,
|
||||
profileById: const {},
|
||||
readOnly: readOnly,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Unit tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void main() {
|
||||
setUpAll(AppTime.initialize);
|
||||
|
||||
test('groupTasksForDashboard splits by run state and excludes other assignees', () {
|
||||
final tasks = [
|
||||
_makeTask(id: 't1', status: ProgrammerTaskStatus.inProgress, assigneeId: 'user1'),
|
||||
_makeTask(id: 't2', status: ProgrammerTaskStatus.inProgress, assigneeId: 'user1'),
|
||||
_makeTask(id: 't3', status: ProgrammerTaskStatus.queued, assigneeId: 'user1'),
|
||||
_makeTask(id: 't4', status: ProgrammerTaskStatus.queued, assigneeId: 'other'),
|
||||
];
|
||||
|
||||
final runStates = <String, ProgrammerRunInfo>{
|
||||
't1': (state: ProgrammerRunState.running, since: null),
|
||||
't2': (state: ProgrammerRunState.paused, since: null),
|
||||
't3': (state: ProgrammerRunState.notStarted, since: null),
|
||||
't4': (state: ProgrammerRunState.notStarted, since: null),
|
||||
};
|
||||
|
||||
final groups = groupTasksForDashboard(tasks, runStates, 'user1');
|
||||
|
||||
expect(groups.running.map((t) => t.id), ['t1']);
|
||||
expect(groups.paused.map((t) => t.id), ['t2']);
|
||||
expect(groups.notStarted.map((t) => t.id), ['t3']);
|
||||
// t4 belongs to 'other' — must not appear
|
||||
expect(groups.running.any((t) => t.id == 't4'), isFalse);
|
||||
expect(groups.paused.any((t) => t.id == 't4'), isFalse);
|
||||
expect(groups.notStarted.any((t) => t.id == 't4'), isFalse);
|
||||
});
|
||||
|
||||
test('completed sorted by completedAt desc, cancelled by cancelledAt desc', () {
|
||||
final base = DateTime(2026, 1, 1);
|
||||
final tasks = [
|
||||
_makeTask(
|
||||
id: 'c1',
|
||||
status: ProgrammerTaskStatus.completed,
|
||||
completedAt: base.add(const Duration(hours: 1)),
|
||||
),
|
||||
_makeTask(
|
||||
id: 'c2',
|
||||
status: ProgrammerTaskStatus.completed,
|
||||
completedAt: base.add(const Duration(hours: 3)),
|
||||
),
|
||||
_makeTask(
|
||||
id: 'x1',
|
||||
status: ProgrammerTaskStatus.cancelled,
|
||||
cancelledAt: base.add(const Duration(hours: 2)),
|
||||
),
|
||||
_makeTask(
|
||||
id: 'x2',
|
||||
status: ProgrammerTaskStatus.cancelled,
|
||||
cancelledAt: base.add(const Duration(hours: 5)),
|
||||
),
|
||||
];
|
||||
|
||||
final runStates = <String, ProgrammerRunInfo>{
|
||||
'c1': (state: ProgrammerRunState.completed, since: null),
|
||||
'c2': (state: ProgrammerRunState.completed, since: null),
|
||||
'x1': (state: ProgrammerRunState.cancelled, since: null),
|
||||
'x2': (state: ProgrammerRunState.cancelled, since: null),
|
||||
};
|
||||
|
||||
final groups = groupTasksForDashboard(tasks, runStates, 'user1');
|
||||
|
||||
expect(groups.completed.map((t) => t.id).toList(), ['c2', 'c1']);
|
||||
expect(groups.cancelled.map((t) => t.id).toList(), ['x2', 'x1']);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Widget tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
testWidgets('Completed and Cancelled start collapsed', (tester) async {
|
||||
final base = DateTime(2026, 1, 1);
|
||||
final completed = List.generate(
|
||||
12,
|
||||
(i) => _makeTask(
|
||||
id: 'c$i',
|
||||
status: ProgrammerTaskStatus.completed,
|
||||
completedAt: base.add(Duration(hours: i)),
|
||||
),
|
||||
);
|
||||
|
||||
final runStates = <String, ProgrammerRunInfo>{
|
||||
for (final t in completed)
|
||||
t.id: (state: ProgrammerRunState.completed, since: null),
|
||||
};
|
||||
|
||||
final groups = groupTasksForDashboard(completed, runStates, 'user1');
|
||||
|
||||
await tester.pumpWidget(_buildSections(groups));
|
||||
await tester.pump();
|
||||
|
||||
// Header is visible
|
||||
expect(find.textContaining('COMPLETED'), findsOneWidget);
|
||||
|
||||
// First task title is NOT visible (collapsed)
|
||||
expect(find.text('Task c0'), findsNothing);
|
||||
|
||||
// Tap the header to expand
|
||||
await tester.tap(find.textContaining('COMPLETED'));
|
||||
await tester.pump();
|
||||
|
||||
// 10 titles visible (first 10 of 12)
|
||||
int visibleCount = 0;
|
||||
for (int i = 0; i < 12; i++) {
|
||||
if (tester.any(find.text('Task c${11 - i}'))) visibleCount++;
|
||||
}
|
||||
expect(visibleCount, 10);
|
||||
|
||||
// "Show all (12)" link is visible
|
||||
expect(find.text('Show all (12)'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('empty sections are hidden', (tester) async {
|
||||
final tasks = [
|
||||
_makeTask(id: 'r1', status: ProgrammerTaskStatus.inProgress),
|
||||
];
|
||||
|
||||
final runStates = <String, ProgrammerRunInfo>{
|
||||
'r1': (state: ProgrammerRunState.running, since: null),
|
||||
};
|
||||
|
||||
final groups = groupTasksForDashboard(tasks, runStates, 'user1');
|
||||
|
||||
await tester.pumpWidget(_buildSections(groups));
|
||||
await tester.pump();
|
||||
|
||||
// 'In progress' header present (at least one widget contains the text)
|
||||
expect(find.textContaining('IN PROGRESS'), findsAtLeastNWidgets(1));
|
||||
|
||||
// 'Cancelled' header not rendered (no cancelled tasks)
|
||||
expect(find.textContaining('CANCELLED'), findsNothing);
|
||||
|
||||
// 'Paused' header not rendered
|
||||
expect(find.textContaining('PAUSED'), findsNothing);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user